kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
ServiceWorker: add jsdoc comments
rodzic
5c49cc0b84
commit
d111c4c2d2
|
@ -10,12 +10,15 @@ import type {
|
||||||
Status as StatusEntity,
|
Status as StatusEntity,
|
||||||
} from 'soapbox/types/entities';
|
} from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
/** Limit before we start grouping device notifications into a single notification. */
|
||||||
const MAX_NOTIFICATIONS = 5;
|
const MAX_NOTIFICATIONS = 5;
|
||||||
|
/** Tag for the grouped notification. */
|
||||||
const GROUP_TAG = 'tag';
|
const GROUP_TAG = 'tag';
|
||||||
|
|
||||||
// https://www.devextent.com/create-service-worker-typescript/
|
// https://www.devextent.com/create-service-worker-typescript/
|
||||||
declare const self: ServiceWorkerGlobalScope;
|
declare const self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
|
/** Soapbox notification data from push event. */
|
||||||
interface NotificationData {
|
interface NotificationData {
|
||||||
access_token?: string,
|
access_token?: string,
|
||||||
preferred_locale: string,
|
preferred_locale: string,
|
||||||
|
@ -26,11 +29,13 @@ interface NotificationData {
|
||||||
count?: number,
|
count?: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ServiceWorker Notification options with extra fields. */
|
||||||
interface ExtendedNotificationOptions extends NotificationOptions {
|
interface ExtendedNotificationOptions extends NotificationOptions {
|
||||||
title: string,
|
title: string,
|
||||||
data: NotificationData,
|
data: NotificationData,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Partial clone of ServiceWorker Notification with mutability. */
|
||||||
interface ClonedNotification {
|
interface ClonedNotification {
|
||||||
body?: string,
|
body?: string,
|
||||||
image?: string,
|
image?: string,
|
||||||
|
@ -40,15 +45,20 @@ interface ClonedNotification {
|
||||||
tag?: string,
|
tag?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Status entitiy from the API (kind of). */
|
||||||
|
// HACK
|
||||||
interface APIStatus extends Omit<StatusEntity, 'media_attachments'> {
|
interface APIStatus extends Omit<StatusEntity, 'media_attachments'> {
|
||||||
media_attachments: { preview_url: string }[],
|
media_attachments: { preview_url: string }[],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Notification entity from the API (kind of). */
|
||||||
|
// HACK
|
||||||
interface APINotification extends Omit<NotificationEntity, 'account' | 'status'> {
|
interface APINotification extends Omit<NotificationEntity, 'account' | 'status'> {
|
||||||
account: AccountEntity,
|
account: AccountEntity,
|
||||||
status?: APIStatus,
|
status?: APIStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Show the actual push notification on the device. */
|
||||||
const notify = (options: ExtendedNotificationOptions): Promise<void> =>
|
const notify = (options: ExtendedNotificationOptions): Promise<void> =>
|
||||||
self.registration.getNotifications().then(notifications => {
|
self.registration.getNotifications().then(notifications => {
|
||||||
if (notifications.length >= MAX_NOTIFICATIONS) { // Reached the maximum number of notifications, proceed with grouping
|
if (notifications.length >= MAX_NOTIFICATIONS) { // Reached the maximum number of notifications, proceed with grouping
|
||||||
|
@ -80,6 +90,7 @@ const notify = (options: ExtendedNotificationOptions): Promise<void> =>
|
||||||
return self.registration.showNotification(options.title, options);
|
return self.registration.showNotification(options.title, options);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Perform an API request to the backend. */
|
||||||
const fetchFromApi = (path: string, method: string, accessToken: string): Promise<APINotification> => {
|
const fetchFromApi = (path: string, method: string, accessToken: string): Promise<APINotification> => {
|
||||||
const url = (new URL(path, self.location.href)).href;
|
const url = (new URL(path, self.location.href)).href;
|
||||||
|
|
||||||
|
@ -100,6 +111,7 @@ const fetchFromApi = (path: string, method: string, accessToken: string): Promis
|
||||||
}).then(res => res.json());
|
}).then(res => res.json());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Create a mutable object that loosely matches the Notification. */
|
||||||
const cloneNotification = (notification: Notification): ClonedNotification => {
|
const cloneNotification = (notification: Notification): ClonedNotification => {
|
||||||
const clone: any = {};
|
const clone: any = {};
|
||||||
let k: string;
|
let k: string;
|
||||||
|
@ -112,12 +124,15 @@ const cloneNotification = (notification: Notification): ClonedNotification => {
|
||||||
return clone as ClonedNotification;
|
return clone as ClonedNotification;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Get translated message for the user's locale. */
|
||||||
const formatMessage = (messageId: string, locale: string, values = {}): string =>
|
const formatMessage = (messageId: string, locale: string, values = {}): string =>
|
||||||
(new IntlMessageFormat(locales[locale][messageId], locale)).format(values) as string;
|
(new IntlMessageFormat(locales[locale][messageId], locale)).format(values) as string;
|
||||||
|
|
||||||
|
/** Strip HTML for display in a native notification. */
|
||||||
const htmlToPlainText = (html: string): string =>
|
const htmlToPlainText = (html: string): string =>
|
||||||
unescape(html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><[^>]*>/g, '\n\n').replace(/<[^>]*>/g, ''));
|
unescape(html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><[^>]*>/g, '\n\n').replace(/<[^>]*>/g, ''));
|
||||||
|
|
||||||
|
/** ServiceWorker `push` event callback. */
|
||||||
const handlePush = (event: PushEvent) => {
|
const handlePush = (event: PushEvent) => {
|
||||||
const { access_token, notification_id, preferred_locale, title, body, icon } = event.data?.json();
|
const { access_token, notification_id, preferred_locale, title, body, icon } = event.data?.json();
|
||||||
|
|
||||||
|
@ -162,24 +177,28 @@ const handlePush = (event: PushEvent) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Native action to open a status on the device. */
|
||||||
const actionExpand = (preferred_locale: string) => ({
|
const actionExpand = (preferred_locale: string) => ({
|
||||||
action: 'expand',
|
action: 'expand',
|
||||||
icon: `/${require('../../images/web-push/web-push-icon_expand.png')}`,
|
icon: `/${require('../../images/web-push/web-push-icon_expand.png')}`,
|
||||||
title: formatMessage('status.show_more', preferred_locale),
|
title: formatMessage('status.show_more', preferred_locale),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Native action to repost status. */
|
||||||
const actionReblog = (preferred_locale: string) => ({
|
const actionReblog = (preferred_locale: string) => ({
|
||||||
action: 'reblog',
|
action: 'reblog',
|
||||||
icon: `/${require('../../images/web-push/web-push-icon_reblog.png')}`,
|
icon: `/${require('../../images/web-push/web-push-icon_reblog.png')}`,
|
||||||
title: formatMessage('status.reblog', preferred_locale),
|
title: formatMessage('status.reblog', preferred_locale),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Native action to like status. */
|
||||||
const actionFavourite = (preferred_locale: string) => ({
|
const actionFavourite = (preferred_locale: string) => ({
|
||||||
action: 'favourite',
|
action: 'favourite',
|
||||||
icon: `/${require('../../images/web-push/web-push-icon_favourite.png')}`,
|
icon: `/${require('../../images/web-push/web-push-icon_favourite.png')}`,
|
||||||
title: formatMessage('status.favourite', preferred_locale),
|
title: formatMessage('status.favourite', preferred_locale),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Get the active tab if possible, or any open tab. */
|
||||||
const findBestClient = (clients: readonly WindowClient[]): WindowClient => {
|
const findBestClient = (clients: readonly WindowClient[]): WindowClient => {
|
||||||
const focusedClient = clients.find(client => client.focused);
|
const focusedClient = clients.find(client => client.focused);
|
||||||
const visibleClient = clients.find(client => client.visibilityState === 'visible');
|
const visibleClient = clients.find(client => client.visibilityState === 'visible');
|
||||||
|
@ -197,6 +216,7 @@ const expandNotification = (notification: Notification) => {
|
||||||
return self.registration.showNotification(newNotification.title, newNotification);
|
return self.registration.showNotification(newNotification.title, newNotification);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Update the native notification, but delete the action (because it was performed). */
|
||||||
const removeActionFromNotification = (notification: Notification, action: string) => {
|
const removeActionFromNotification = (notification: Notification, action: string) => {
|
||||||
const newNotification = cloneNotification(notification);
|
const newNotification = cloneNotification(notification);
|
||||||
|
|
||||||
|
@ -205,6 +225,7 @@ const removeActionFromNotification = (notification: Notification, action: string
|
||||||
return self.registration.showNotification(newNotification.title, newNotification);
|
return self.registration.showNotification(newNotification.title, newNotification);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Open a URL on the device. */
|
||||||
const openUrl = (url: string) =>
|
const openUrl = (url: string) =>
|
||||||
self.clients.matchAll({ type: 'window' }).then(clientList => {
|
self.clients.matchAll({ type: 'window' }).then(clientList => {
|
||||||
if (clientList.length === 0) {
|
if (clientList.length === 0) {
|
||||||
|
@ -215,6 +236,7 @@ const openUrl = (url: string) =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Callback when a native notification is clicked/touched on the device. */
|
||||||
const handleNotificationClick = (event: NotificationEvent) => {
|
const handleNotificationClick = (event: NotificationEvent) => {
|
||||||
const reactToNotificationClick = new Promise((resolve, reject) => {
|
const reactToNotificationClick = new Promise((resolve, reject) => {
|
||||||
if (event.action) {
|
if (event.action) {
|
||||||
|
@ -238,5 +260,6 @@ const handleNotificationClick = (event: NotificationEvent) => {
|
||||||
event.waitUntil(reactToNotificationClick);
|
event.waitUntil(reactToNotificationClick);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ServiceWorker event listeners
|
||||||
self.addEventListener('push', handlePush);
|
self.addEventListener('push', handlePush);
|
||||||
self.addEventListener('notificationclick', handleNotificationClick);
|
self.addEventListener('notificationclick', handleNotificationClick);
|
||||||
|
|
Ładowanie…
Reference in New Issue