kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'vite-fixes' into 'develop'
Vite fixes See merge request soapbox-pub/soapbox!2679environments/review-develop-3zknud/deployments/3832
commit
d34eaa8be2
|
@ -9,6 +9,7 @@
|
||||||
/.vs/
|
/.vs/
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
/junit.xml
|
/junit.xml
|
||||||
|
*.timestamp-*
|
||||||
|
|
||||||
/static/
|
/static/
|
||||||
/static-test/
|
/static-test/
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import './polyfills';
|
import './polyfills';
|
||||||
|
|
||||||
import * as OfflinePluginRuntime from '@lcdp/offline-plugin/runtime';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
@ -40,10 +39,4 @@ ready(() => {
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
root.render(<Soapbox />);
|
root.render(<Soapbox />);
|
||||||
|
|
||||||
if (BuildConfig.NODE_ENV === 'production') {
|
|
||||||
// avoid offline in dev mode because it's harder to debug
|
|
||||||
// https://github.com/NekR/offline-plugin/pull/201#issuecomment-285133572
|
|
||||||
OfflinePluginRuntime.install();
|
|
||||||
}
|
|
||||||
});
|
});
|
|
@ -1 +1,272 @@
|
||||||
import './web-push-notifications';
|
/// <reference lib="webworker" />
|
||||||
|
import IntlMessageFormat from 'intl-messageformat';
|
||||||
|
import 'intl-pluralrules';
|
||||||
|
import unescape from 'lodash/unescape';
|
||||||
|
|
||||||
|
import locales from './web-push-locales';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Account as AccountEntity,
|
||||||
|
Notification as NotificationEntity,
|
||||||
|
Status as StatusEntity,
|
||||||
|
} from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
/** Limit before we start grouping device notifications into a single notification. */
|
||||||
|
const MAX_NOTIFICATIONS = 5;
|
||||||
|
/** Tag for the grouped notification. */
|
||||||
|
const GROUP_TAG = 'tag';
|
||||||
|
|
||||||
|
// https://www.devextent.com/create-service-worker-typescript/
|
||||||
|
declare const self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
|
/** Soapbox notification data from push event. */
|
||||||
|
interface NotificationData {
|
||||||
|
access_token?: string
|
||||||
|
count?: number
|
||||||
|
hiddenBody?: string
|
||||||
|
hiddenImage?: string
|
||||||
|
id?: string
|
||||||
|
preferred_locale: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ServiceWorker Notification options with extra fields. */
|
||||||
|
interface ExtendedNotificationOptions extends NotificationOptions {
|
||||||
|
data: NotificationData
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Partial clone of ServiceWorker Notification with mutability. */
|
||||||
|
interface ClonedNotification {
|
||||||
|
actions?: NotificationAction[]
|
||||||
|
body?: string
|
||||||
|
data: NotificationData
|
||||||
|
image?: string
|
||||||
|
tag?: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Status entitiy from the API (kind of). */
|
||||||
|
// HACK
|
||||||
|
interface APIStatus extends Omit<StatusEntity, 'media_attachments'> {
|
||||||
|
media_attachments: { preview_url: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Notification entity from the API (kind of). */
|
||||||
|
// HACK
|
||||||
|
interface APINotification extends Omit<NotificationEntity, 'account' | 'status'> {
|
||||||
|
account: AccountEntity
|
||||||
|
status?: APIStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show the actual push notification on the device. */
|
||||||
|
const notify = (options: ExtendedNotificationOptions): Promise<void> =>
|
||||||
|
self.registration.getNotifications().then(notifications => {
|
||||||
|
if (notifications.length >= MAX_NOTIFICATIONS) { // Reached the maximum number of notifications, proceed with grouping
|
||||||
|
const group: ClonedNotification = {
|
||||||
|
title: formatMessage('notifications.group', options.data.preferred_locale, { count: notifications.length + 1 }),
|
||||||
|
body: notifications.map(notification => notification.title).join('\n'),
|
||||||
|
tag: GROUP_TAG,
|
||||||
|
data: {
|
||||||
|
url: (new URL('/notifications', self.location.href)).href,
|
||||||
|
count: notifications.length + 1,
|
||||||
|
preferred_locale: options.data.preferred_locale,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
notifications.forEach(notification => notification.close());
|
||||||
|
|
||||||
|
return self.registration.showNotification(group.title, group);
|
||||||
|
} else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) { // Already grouped, proceed with appending the notification to the group
|
||||||
|
const group = cloneNotification(notifications[0]);
|
||||||
|
const count = (group.data.count || 0) + 1;
|
||||||
|
|
||||||
|
group.title = formatMessage('notifications.group', options.data.preferred_locale, { count });
|
||||||
|
group.body = `${options.title}\n${group.body}`;
|
||||||
|
group.data = { ...group.data, count };
|
||||||
|
|
||||||
|
return self.registration.showNotification(group.title, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 url = (new URL(path, self.location.href)).href;
|
||||||
|
|
||||||
|
return fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
|
||||||
|
method: method,
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
throw new Error(String(res.status));
|
||||||
|
}
|
||||||
|
}).then(res => res.json());
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Create a mutable object that loosely matches the Notification. */
|
||||||
|
const cloneNotification = (notification: Notification): ClonedNotification => {
|
||||||
|
const clone: any = {};
|
||||||
|
let k: string;
|
||||||
|
|
||||||
|
// Object.assign() does not work with notifications
|
||||||
|
for (k in notification) {
|
||||||
|
clone[k] = (notification as any)[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone as ClonedNotification;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Get translated message for the user's locale. */
|
||||||
|
const formatMessage = (messageId: string, locale: string, values = {}): string =>
|
||||||
|
(new IntlMessageFormat(locales[locale][messageId], locale)).format(values) as string;
|
||||||
|
|
||||||
|
/** Strip HTML for display in a native notification. */
|
||||||
|
const htmlToPlainText = (html: string): string =>
|
||||||
|
unescape(html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><[^>]*>/g, '\n\n').replace(/<[^>]*>/g, ''));
|
||||||
|
|
||||||
|
/** ServiceWorker `push` event callback. */
|
||||||
|
const handlePush = (event: PushEvent) => {
|
||||||
|
if (!event.data) {
|
||||||
|
console.error('An empty web push event was received.', { event });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json();
|
||||||
|
|
||||||
|
// Placeholder until more information can be loaded
|
||||||
|
event.waitUntil(
|
||||||
|
fetchFromApi(`/api/v1/notifications/${notification_id}`, 'get', access_token).then(notification => {
|
||||||
|
const options: ExtendedNotificationOptions = {
|
||||||
|
title: formatMessage(`notification.${notification.type}`, preferred_locale, { name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }),
|
||||||
|
body: notification.status && htmlToPlainText(notification.status.content),
|
||||||
|
icon: notification.account.avatar_static,
|
||||||
|
timestamp: notification.created_at && Number(new Date(notification.created_at)),
|
||||||
|
tag: notification.id,
|
||||||
|
image: notification.status?.media_attachments[0]?.preview_url,
|
||||||
|
data: { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.acct}/posts/${notification.status.id}` : `/@${notification.account.acct}` },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notification.status?.spoiler_text || notification.status?.sensitive) {
|
||||||
|
options.data.hiddenBody = htmlToPlainText(notification.status?.content);
|
||||||
|
options.data.hiddenImage = notification.status?.media_attachments[0]?.preview_url;
|
||||||
|
|
||||||
|
if (notification.status?.spoiler_text) {
|
||||||
|
options.body = notification.status.spoiler_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.image = undefined;
|
||||||
|
options.actions = [actionExpand(preferred_locale)];
|
||||||
|
} else if (notification.type === 'mention') {
|
||||||
|
options.actions = [actionReblog(preferred_locale), actionFavourite(preferred_locale)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return notify(options);
|
||||||
|
}).catch(() => {
|
||||||
|
return notify({
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
icon,
|
||||||
|
tag: notification_id,
|
||||||
|
timestamp: Number(new Date()),
|
||||||
|
data: { access_token, preferred_locale, url: '/notifications' },
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Native action to open a status on the device. */
|
||||||
|
const actionExpand = (preferred_locale: string) => ({
|
||||||
|
action: 'expand',
|
||||||
|
icon: `/${require('../../assets/images/web-push/web-push-icon_expand.png')}`,
|
||||||
|
title: formatMessage('status.show_more', preferred_locale),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Native action to repost status. */
|
||||||
|
const actionReblog = (preferred_locale: string) => ({
|
||||||
|
action: 'reblog',
|
||||||
|
icon: `/${require('../../assets/images/web-push/web-push-icon_reblog.png')}`,
|
||||||
|
title: formatMessage('status.reblog', preferred_locale),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Native action to like status. */
|
||||||
|
const actionFavourite = (preferred_locale: string) => ({
|
||||||
|
action: 'favourite',
|
||||||
|
icon: `/${require('../../assets/images/web-push/web-push-icon_favourite.png')}`,
|
||||||
|
title: formatMessage('status.favourite', preferred_locale),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Get the active tab if possible, or any open tab. */
|
||||||
|
const findBestClient = (clients: readonly WindowClient[]): WindowClient => {
|
||||||
|
const focusedClient = clients.find(client => client.focused);
|
||||||
|
const visibleClient = clients.find(client => client.visibilityState === 'visible');
|
||||||
|
|
||||||
|
return focusedClient || visibleClient || clients[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Update a notification with CW to display the full status. */
|
||||||
|
const expandNotification = (notification: Notification) => {
|
||||||
|
const newNotification = cloneNotification(notification);
|
||||||
|
|
||||||
|
newNotification.body = newNotification.data.hiddenBody;
|
||||||
|
newNotification.image = newNotification.data.hiddenImage;
|
||||||
|
newNotification.actions = [actionReblog(notification.data.preferred_locale), actionFavourite(notification.data.preferred_locale)];
|
||||||
|
|
||||||
|
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 newNotification = cloneNotification(notification);
|
||||||
|
|
||||||
|
newNotification.actions = newNotification.actions?.filter(item => item.action !== action);
|
||||||
|
|
||||||
|
return self.registration.showNotification(newNotification.title, newNotification);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Open a URL on the device. */
|
||||||
|
const openUrl = (url: string) =>
|
||||||
|
self.clients.matchAll({ type: 'window' }).then(clientList => {
|
||||||
|
if (clientList.length === 0) {
|
||||||
|
return self.clients.openWindow(url);
|
||||||
|
} else {
|
||||||
|
const client = findBestClient(clientList);
|
||||||
|
return client.navigate(url).then(client => client?.focus());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Callback when a native notification is clicked/touched on the device. */
|
||||||
|
const handleNotificationClick = (event: NotificationEvent) => {
|
||||||
|
const reactToNotificationClick = new Promise((resolve, reject) => {
|
||||||
|
if (event.action) {
|
||||||
|
if (event.action === 'expand') {
|
||||||
|
resolve(expandNotification(event.notification));
|
||||||
|
} else if (event.action === 'reblog') {
|
||||||
|
const { data } = event.notification;
|
||||||
|
resolve(fetchFromApi(`/api/v1/statuses/${data.id}/reblog`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'reblog')));
|
||||||
|
} else if (event.action === 'favourite') {
|
||||||
|
const { data } = event.notification;
|
||||||
|
resolve(fetchFromApi(`/api/v1/statuses/${data.id}/favourite`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'favourite')));
|
||||||
|
} else {
|
||||||
|
reject(`Unknown action: ${event.action}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
event.notification.close();
|
||||||
|
resolve(openUrl(event.notification.data.url));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.waitUntil(reactToNotificationClick);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ServiceWorker event listeners
|
||||||
|
self.addEventListener('push', handlePush);
|
||||||
|
self.addEventListener('notificationclick', handleNotificationClick);
|
||||||
|
|
|
@ -1,272 +0,0 @@
|
||||||
/// <reference lib="webworker" />
|
|
||||||
import IntlMessageFormat from 'intl-messageformat';
|
|
||||||
import 'intl-pluralrules';
|
|
||||||
import unescape from 'lodash/unescape';
|
|
||||||
|
|
||||||
import locales from './web-push-locales';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
Account as AccountEntity,
|
|
||||||
Notification as NotificationEntity,
|
|
||||||
Status as StatusEntity,
|
|
||||||
} from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
/** Limit before we start grouping device notifications into a single notification. */
|
|
||||||
const MAX_NOTIFICATIONS = 5;
|
|
||||||
/** Tag for the grouped notification. */
|
|
||||||
const GROUP_TAG = 'tag';
|
|
||||||
|
|
||||||
// https://www.devextent.com/create-service-worker-typescript/
|
|
||||||
declare const self: ServiceWorkerGlobalScope;
|
|
||||||
|
|
||||||
/** Soapbox notification data from push event. */
|
|
||||||
interface NotificationData {
|
|
||||||
access_token?: string
|
|
||||||
count?: number
|
|
||||||
hiddenBody?: string
|
|
||||||
hiddenImage?: string
|
|
||||||
id?: string
|
|
||||||
preferred_locale: string
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ServiceWorker Notification options with extra fields. */
|
|
||||||
interface ExtendedNotificationOptions extends NotificationOptions {
|
|
||||||
data: NotificationData
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Partial clone of ServiceWorker Notification with mutability. */
|
|
||||||
interface ClonedNotification {
|
|
||||||
actions?: NotificationAction[]
|
|
||||||
body?: string
|
|
||||||
data: NotificationData
|
|
||||||
image?: string
|
|
||||||
tag?: string
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Status entitiy from the API (kind of). */
|
|
||||||
// HACK
|
|
||||||
interface APIStatus extends Omit<StatusEntity, 'media_attachments'> {
|
|
||||||
media_attachments: { preview_url: string }[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Notification entity from the API (kind of). */
|
|
||||||
// HACK
|
|
||||||
interface APINotification extends Omit<NotificationEntity, 'account' | 'status'> {
|
|
||||||
account: AccountEntity
|
|
||||||
status?: APIStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Show the actual push notification on the device. */
|
|
||||||
const notify = (options: ExtendedNotificationOptions): Promise<void> =>
|
|
||||||
self.registration.getNotifications().then(notifications => {
|
|
||||||
if (notifications.length >= MAX_NOTIFICATIONS) { // Reached the maximum number of notifications, proceed with grouping
|
|
||||||
const group: ClonedNotification = {
|
|
||||||
title: formatMessage('notifications.group', options.data.preferred_locale, { count: notifications.length + 1 }),
|
|
||||||
body: notifications.map(notification => notification.title).join('\n'),
|
|
||||||
tag: GROUP_TAG,
|
|
||||||
data: {
|
|
||||||
url: (new URL('/notifications', self.location.href)).href,
|
|
||||||
count: notifications.length + 1,
|
|
||||||
preferred_locale: options.data.preferred_locale,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
notifications.forEach(notification => notification.close());
|
|
||||||
|
|
||||||
return self.registration.showNotification(group.title, group);
|
|
||||||
} else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) { // Already grouped, proceed with appending the notification to the group
|
|
||||||
const group = cloneNotification(notifications[0]);
|
|
||||||
const count = (group.data.count || 0) + 1;
|
|
||||||
|
|
||||||
group.title = formatMessage('notifications.group', options.data.preferred_locale, { count });
|
|
||||||
group.body = `${options.title}\n${group.body}`;
|
|
||||||
group.data = { ...group.data, count };
|
|
||||||
|
|
||||||
return self.registration.showNotification(group.title, group);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 url = (new URL(path, self.location.href)).href;
|
|
||||||
|
|
||||||
return fetch(url, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
|
|
||||||
method: method,
|
|
||||||
credentials: 'include',
|
|
||||||
}).then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
return res;
|
|
||||||
} else {
|
|
||||||
throw new Error(String(res.status));
|
|
||||||
}
|
|
||||||
}).then(res => res.json());
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Create a mutable object that loosely matches the Notification. */
|
|
||||||
const cloneNotification = (notification: Notification): ClonedNotification => {
|
|
||||||
const clone: any = {};
|
|
||||||
let k: string;
|
|
||||||
|
|
||||||
// Object.assign() does not work with notifications
|
|
||||||
for (k in notification) {
|
|
||||||
clone[k] = (notification as any)[k];
|
|
||||||
}
|
|
||||||
|
|
||||||
return clone as ClonedNotification;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Get translated message for the user's locale. */
|
|
||||||
const formatMessage = (messageId: string, locale: string, values = {}): string =>
|
|
||||||
(new IntlMessageFormat(locales[locale][messageId], locale)).format(values) as string;
|
|
||||||
|
|
||||||
/** Strip HTML for display in a native notification. */
|
|
||||||
const htmlToPlainText = (html: string): string =>
|
|
||||||
unescape(html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><[^>]*>/g, '\n\n').replace(/<[^>]*>/g, ''));
|
|
||||||
|
|
||||||
/** ServiceWorker `push` event callback. */
|
|
||||||
const handlePush = (event: PushEvent) => {
|
|
||||||
if (!event.data) {
|
|
||||||
console.error('An empty web push event was received.', { event });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json();
|
|
||||||
|
|
||||||
// Placeholder until more information can be loaded
|
|
||||||
event.waitUntil(
|
|
||||||
fetchFromApi(`/api/v1/notifications/${notification_id}`, 'get', access_token).then(notification => {
|
|
||||||
const options: ExtendedNotificationOptions = {
|
|
||||||
title: formatMessage(`notification.${notification.type}`, preferred_locale, { name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }),
|
|
||||||
body: notification.status && htmlToPlainText(notification.status.content),
|
|
||||||
icon: notification.account.avatar_static,
|
|
||||||
timestamp: notification.created_at && Number(new Date(notification.created_at)),
|
|
||||||
tag: notification.id,
|
|
||||||
image: notification.status?.media_attachments[0]?.preview_url,
|
|
||||||
data: { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.acct}/posts/${notification.status.id}` : `/@${notification.account.acct}` },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (notification.status?.spoiler_text || notification.status?.sensitive) {
|
|
||||||
options.data.hiddenBody = htmlToPlainText(notification.status?.content);
|
|
||||||
options.data.hiddenImage = notification.status?.media_attachments[0]?.preview_url;
|
|
||||||
|
|
||||||
if (notification.status?.spoiler_text) {
|
|
||||||
options.body = notification.status.spoiler_text;
|
|
||||||
}
|
|
||||||
|
|
||||||
options.image = undefined;
|
|
||||||
options.actions = [actionExpand(preferred_locale)];
|
|
||||||
} else if (notification.type === 'mention') {
|
|
||||||
options.actions = [actionReblog(preferred_locale), actionFavourite(preferred_locale)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return notify(options);
|
|
||||||
}).catch(() => {
|
|
||||||
return notify({
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
icon,
|
|
||||||
tag: notification_id,
|
|
||||||
timestamp: Number(new Date()),
|
|
||||||
data: { access_token, preferred_locale, url: '/notifications' },
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Native action to open a status on the device. */
|
|
||||||
const actionExpand = (preferred_locale: string) => ({
|
|
||||||
action: 'expand',
|
|
||||||
icon: `/${require('../../assets/images/web-push/web-push-icon_expand.png')}`,
|
|
||||||
title: formatMessage('status.show_more', preferred_locale),
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Native action to repost status. */
|
|
||||||
const actionReblog = (preferred_locale: string) => ({
|
|
||||||
action: 'reblog',
|
|
||||||
icon: `/${require('../../assets/images/web-push/web-push-icon_reblog.png')}`,
|
|
||||||
title: formatMessage('status.reblog', preferred_locale),
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Native action to like status. */
|
|
||||||
const actionFavourite = (preferred_locale: string) => ({
|
|
||||||
action: 'favourite',
|
|
||||||
icon: `/${require('../../assets/images/web-push/web-push-icon_favourite.png')}`,
|
|
||||||
title: formatMessage('status.favourite', preferred_locale),
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Get the active tab if possible, or any open tab. */
|
|
||||||
const findBestClient = (clients: readonly WindowClient[]): WindowClient => {
|
|
||||||
const focusedClient = clients.find(client => client.focused);
|
|
||||||
const visibleClient = clients.find(client => client.visibilityState === 'visible');
|
|
||||||
|
|
||||||
return focusedClient || visibleClient || clients[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Update a notification with CW to display the full status. */
|
|
||||||
const expandNotification = (notification: Notification) => {
|
|
||||||
const newNotification = cloneNotification(notification);
|
|
||||||
|
|
||||||
newNotification.body = newNotification.data.hiddenBody;
|
|
||||||
newNotification.image = newNotification.data.hiddenImage;
|
|
||||||
newNotification.actions = [actionReblog(notification.data.preferred_locale), actionFavourite(notification.data.preferred_locale)];
|
|
||||||
|
|
||||||
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 newNotification = cloneNotification(notification);
|
|
||||||
|
|
||||||
newNotification.actions = newNotification.actions?.filter(item => item.action !== action);
|
|
||||||
|
|
||||||
return self.registration.showNotification(newNotification.title, newNotification);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Open a URL on the device. */
|
|
||||||
const openUrl = (url: string) =>
|
|
||||||
self.clients.matchAll({ type: 'window' }).then(clientList => {
|
|
||||||
if (clientList.length === 0) {
|
|
||||||
return self.clients.openWindow(url);
|
|
||||||
} else {
|
|
||||||
const client = findBestClient(clientList);
|
|
||||||
return client.navigate(url).then(client => client?.focus());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Callback when a native notification is clicked/touched on the device. */
|
|
||||||
const handleNotificationClick = (event: NotificationEvent) => {
|
|
||||||
const reactToNotificationClick = new Promise((resolve, reject) => {
|
|
||||||
if (event.action) {
|
|
||||||
if (event.action === 'expand') {
|
|
||||||
resolve(expandNotification(event.notification));
|
|
||||||
} else if (event.action === 'reblog') {
|
|
||||||
const { data } = event.notification;
|
|
||||||
resolve(fetchFromApi(`/api/v1/statuses/${data.id}/reblog`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'reblog')));
|
|
||||||
} else if (event.action === 'favourite') {
|
|
||||||
const { data } = event.notification;
|
|
||||||
resolve(fetchFromApi(`/api/v1/statuses/${data.id}/favourite`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'favourite')));
|
|
||||||
} else {
|
|
||||||
reject(`Unknown action: ${event.action}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
event.notification.close();
|
|
||||||
resolve(openUrl(event.notification.data.url));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
event.waitUntil(reactToNotificationClick);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ServiceWorker event listeners
|
|
||||||
self.addEventListener('push', handlePush);
|
|
||||||
self.addEventListener('notificationclick', handleNotificationClick);
|
|
|
@ -49,7 +49,6 @@
|
||||||
"@fontsource/roboto-mono": "^4.5.8",
|
"@fontsource/roboto-mono": "^4.5.8",
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@jest/globals": "^29.0.0",
|
"@jest/globals": "^29.0.0",
|
||||||
"@lcdp/offline-plugin": "^5.1.0",
|
|
||||||
"@metamask/providers": "^10.0.0",
|
"@metamask/providers": "^10.0.0",
|
||||||
"@popperjs/core": "^2.11.5",
|
"@popperjs/core": "^2.11.5",
|
||||||
"@reach/combobox": "^0.18.0",
|
"@reach/combobox": "^0.18.0",
|
||||||
|
|
|
@ -13,6 +13,14 @@ export default defineConfig({
|
||||||
// Relative to the root
|
// Relative to the root
|
||||||
outDir: '../static',
|
outDir: '../static',
|
||||||
assetsDir: 'packs',
|
assetsDir: 'packs',
|
||||||
|
assetsInlineLimit: 0,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
assetFileNames: 'packs/assets/[name]-[hash].[ext]',
|
||||||
|
chunkFileNames: 'packs/js/[name]-[hash].js',
|
||||||
|
entryFileNames: 'packs/[name]-[hash].js',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3036,
|
port: 3036,
|
||||||
|
|
30
yarn.lock
30
yarn.lock
|
@ -1861,17 +1861,6 @@
|
||||||
"@jridgewell/resolve-uri" "^3.1.0"
|
"@jridgewell/resolve-uri" "^3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||||
|
|
||||||
"@lcdp/offline-plugin@^5.1.0":
|
|
||||||
version "5.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@lcdp/offline-plugin/-/offline-plugin-5.1.0.tgz#826f3e10d618711bd002afd674edb36dc1d9a792"
|
|
||||||
integrity sha512-GXdAsFyv+OoVLBKvog+oiOZUdgWYjWjH7O8Btfy1VLQsnnC4CRQ+fv/pYKazFgofX/wOO3MhcszoA5ARvNNlLw==
|
|
||||||
dependencies:
|
|
||||||
deep-extend "^0.5.1"
|
|
||||||
ejs "^2.3.4"
|
|
||||||
loader-utils "0.2.x"
|
|
||||||
minimatch "^3.0.3"
|
|
||||||
slash "^1.0.0"
|
|
||||||
|
|
||||||
"@mdn/browser-compat-data@^3.3.14":
|
"@mdn/browser-compat-data@^3.3.14":
|
||||||
version "3.3.14"
|
version "3.3.14"
|
||||||
resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-3.3.14.tgz#b72a37c654e598f9ae6f8335faaee182bebc6b28"
|
resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-3.3.14.tgz#b72a37c654e598f9ae6f8335faaee182bebc6b28"
|
||||||
|
@ -4247,11 +4236,6 @@ dedent@^0.7.0:
|
||||||
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
|
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
|
||||||
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
|
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
|
||||||
|
|
||||||
deep-extend@^0.5.1:
|
|
||||||
version "0.5.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f"
|
|
||||||
integrity sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==
|
|
||||||
|
|
||||||
deep-is@^0.1.3, deep-is@~0.1.3:
|
deep-is@^0.1.3, deep-is@~0.1.3:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||||
|
@ -4438,11 +4422,6 @@ ecdsa-sig-formatter@1.0.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
ejs@^2.3.4:
|
|
||||||
version "2.7.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
|
|
||||||
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
|
|
||||||
|
|
||||||
ejs@^3.1.6:
|
ejs@^3.1.6:
|
||||||
version "3.1.9"
|
version "3.1.9"
|
||||||
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361"
|
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361"
|
||||||
|
@ -6795,7 +6774,7 @@ loader-runner@^4.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
|
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
|
||||||
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
|
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
|
||||||
|
|
||||||
loader-utils@0.2.x, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^2.0.3:
|
loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^2.0.3:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
|
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
|
||||||
integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==
|
integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==
|
||||||
|
@ -7140,7 +7119,7 @@ mini-svg-data-uri@^1.2.3:
|
||||||
resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.3.tgz#43177b2e93766ba338931a3e2a84a3dfd3a222b8"
|
resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.3.tgz#43177b2e93766ba338931a3e2a84a3dfd3a222b8"
|
||||||
integrity sha512-gSfqpMRC8IxghvMcxzzmMnWpXAChSA+vy4cia33RgerMS8Fex95akUyQZPbxJJmeBGiGmK7n/1OpUX8ksRjIdA==
|
integrity sha512-gSfqpMRC8IxghvMcxzzmMnWpXAChSA+vy4cia33RgerMS8Fex95akUyQZPbxJJmeBGiGmK7n/1OpUX8ksRjIdA==
|
||||||
|
|
||||||
minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2:
|
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||||
|
@ -8905,11 +8884,6 @@ sisteransi@^1.0.5:
|
||||||
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
|
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
|
||||||
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
|
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
|
||||||
|
|
||||||
slash@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
|
|
||||||
integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=
|
|
||||||
|
|
||||||
slash@^3.0.0:
|
slash@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||||
|
|
Ładowanie…
Reference in New Issue