summaryrefslogtreecommitdiff
path: root/packages/sw/src
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2022-05-01 15:08:25 +0900
committerGitHub <noreply@github.com>2022-05-01 15:08:25 +0900
commitc5048ee9935869e793bc941fda326d83d18ebbe8 (patch)
tree5c1df069741ecbd3548a702da683baf53d3bae5c /packages/sw/src
parentressurect deepcopy (diff)
parentrefactor(client): refactor import-export to use Composition API (#8579) (diff)
downloadmisskey-c5048ee9935869e793bc941fda326d83d18ebbe8.tar.gz
misskey-c5048ee9935869e793bc941fda326d83d18ebbe8.tar.bz2
misskey-c5048ee9935869e793bc941fda326d83d18ebbe8.zip
Merge branch 'develop' into pizzax-indexeddb
Diffstat (limited to 'packages/sw/src')
-rw-r--r--packages/sw/src/filters/user.ts14
-rw-r--r--packages/sw/src/scripts/create-notification.ts237
-rw-r--r--packages/sw/src/scripts/get-account-from-id.ts7
-rw-r--r--packages/sw/src/scripts/get-user-name.ts3
-rw-r--r--packages/sw/src/scripts/i18n.ts29
-rw-r--r--packages/sw/src/scripts/lang.ts47
-rw-r--r--packages/sw/src/scripts/login-id.ts11
-rw-r--r--packages/sw/src/scripts/notification-read.ts50
-rw-r--r--packages/sw/src/scripts/operations.ts70
-rw-r--r--packages/sw/src/sw.ts200
-rw-r--r--packages/sw/src/types.ts31
11 files changed, 699 insertions, 0 deletions
diff --git a/packages/sw/src/filters/user.ts b/packages/sw/src/filters/user.ts
new file mode 100644
index 0000000000..09437eb19a
--- /dev/null
+++ b/packages/sw/src/filters/user.ts
@@ -0,0 +1,14 @@
+import * as misskey from 'misskey-js';
+import * as Acct from 'misskey-js/built/acct';
+
+export const acct = (user: misskey.Acct) => {
+ return Acct.toString(user);
+};
+
+export const userName = (user: misskey.entities.User) => {
+ return user.name || user.username;
+};
+
+export const userPage = (user: misskey.Acct, path?, absolute = false) => {
+ return `${absolute ? origin : ''}/@${acct(user)}${(path ? `/${path}` : '')}`;
+};
diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts
new file mode 100644
index 0000000000..6d7ba7d524
--- /dev/null
+++ b/packages/sw/src/scripts/create-notification.ts
@@ -0,0 +1,237 @@
+/*
+ * Notification manager for SW
+ */
+declare var self: ServiceWorkerGlobalScope;
+
+import { swLang } from '@/scripts/lang';
+import { cli } from '@/scripts/operations';
+import { pushNotificationDataMap } from '@/types';
+import getUserName from '@/scripts/get-user-name';
+import { I18n } from '@/scripts/i18n';
+import { getAccountFromId } from '@/scripts/get-account-from-id';
+
+export async function createNotification<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]) {
+ const n = await composeNotification(data);
+
+ if (n) {
+ return self.registration.showNotification(...n);
+ } else {
+ console.error('Could not compose notification', data);
+ return createEmptyNotification();
+ }
+}
+
+async function composeNotification<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]): Promise<[string, NotificationOptions] | null> {
+ if (!swLang.i18n) swLang.fetchLocale();
+ const i18n = await swLang.i18n as I18n<any>;
+ const { t } = i18n;
+ switch (data.type) {
+ /*
+ case 'driveFileCreated': // TODO (Server Side)
+ return [t('_notification.fileUploaded'), {
+ body: body.name,
+ icon: body.url,
+ data
+ }];
+ */
+ case 'notification':
+ switch (data.body.type) {
+ case 'follow':
+ // users/showの型定義をswos.apiへ当てはめるのが困難なのでapiFetch.requestを直接使用
+ const account = await getAccountFromId(data.userId);
+ if (!account) return null;
+ const userDetail = await cli.request('users/show', { userId: data.body.userId }, account.token);
+ return [t('_notification.youWereFollowed'), {
+ body: getUserName(data.body.user),
+ icon: data.body.user.avatarUrl,
+ data,
+ actions: userDetail.isFollowing ? [] : [
+ {
+ action: 'follow',
+ title: t('_notification._actions.followBack')
+ }
+ ],
+ }];
+
+ case 'mention':
+ return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), {
+ body: data.body.note.text || '',
+ icon: data.body.user.avatarUrl,
+ data,
+ actions: [
+ {
+ action: 'reply',
+ title: t('_notification._actions.reply')
+ }
+ ],
+ }];
+
+ case 'reply':
+ return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), {
+ body: data.body.note.text || '',
+ icon: data.body.user.avatarUrl,
+ data,
+ actions: [
+ {
+ action: 'reply',
+ title: t('_notification._actions.reply')
+ }
+ ],
+ }];
+
+ case 'renote':
+ return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), {
+ body: data.body.note.text || '',
+ icon: data.body.user.avatarUrl,
+ data,
+ actions: [
+ {
+ action: 'showUser',
+ title: getUserName(data.body.user)
+ }
+ ],
+ }];
+
+ case 'quote':
+ return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
+ body: data.body.note.text || '',
+ icon: data.body.user.avatarUrl,
+ data,
+ actions: [
+ {
+ action: 'reply',
+ title: t('_notification._actions.reply')
+ },
+ ...((data.body.note.visibility === 'public' || data.body.note.visibility === 'home') ? [
+ {
+ action: 'renote',
+ title: t('_notification._actions.renote')
+ }
+ ] : [])
+ ],
+ }];
+
+ case 'reaction':
+ return [`${data.body.reaction} ${getUserName(data.body.user)}`, {
+ body: data.body.note.text || '',
+ icon: data.body.user.avatarUrl,
+ data,
+ actions: [
+ {
+ action: 'showUser',
+ title: getUserName(data.body.user)
+ }
+ ],
+ }];
+
+ case 'pollVote':
+ return [t('_notification.youGotPoll', { name: getUserName(data.body.user) }), {
+ body: data.body.note.text || '',
+ icon: data.body.user.avatarUrl,
+ data,
+ }];
+
+ case 'pollEnded':
+ return [t('_notification.pollEnded'), {
+ body: data.body.note.text || '',
+ data,
+ }];
+
+ case 'receiveFollowRequest':
+ return [t('_notification.youReceivedFollowRequest'), {
+ body: getUserName(data.body.user),
+ icon: data.body.user.avatarUrl,
+ data,
+ actions: [
+ {
+ action: 'accept',
+ title: t('accept')
+ },
+ {
+ action: 'reject',
+ title: t('reject')
+ }
+ ],
+ }];
+
+ case 'followRequestAccepted':
+ return [t('_notification.yourFollowRequestAccepted'), {
+ body: getUserName(data.body.user),
+ icon: data.body.user.avatarUrl,
+ data,
+ }];
+
+ case 'groupInvited':
+ return [t('_notification.youWereInvitedToGroup', { userName: getUserName(data.body.user) }), {
+ body: data.body.invitation.group.name,
+ data,
+ actions: [
+ {
+ action: 'accept',
+ title: t('accept')
+ },
+ {
+ action: 'reject',
+ title: t('reject')
+ }
+ ],
+ }];
+
+ case 'app':
+ return [data.body.header || data.body.body, {
+ body: data.body.header && data.body.body,
+ icon: data.body.icon,
+ data
+ }];
+
+ default:
+ return null;
+ }
+ case 'unreadMessagingMessage':
+ if (data.body.groupId === null) {
+ return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), {
+ icon: data.body.user.avatarUrl,
+ tag: `messaging:user:${data.body.userId}`,
+ data,
+ renotify: true,
+ }];
+ }
+ return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), {
+ icon: data.body.user.avatarUrl,
+ tag: `messaging:group:${data.body.groupId}`,
+ data,
+ renotify: true,
+ }];
+ default:
+ return null;
+ }
+}
+
+export async function createEmptyNotification() {
+ return new Promise<void>(async res => {
+ if (!swLang.i18n) swLang.fetchLocale();
+ const i18n = await swLang.i18n as I18n<any>;
+ const { t } = i18n;
+
+ await self.registration.showNotification(
+ t('_notification.emptyPushNotificationMessage'),
+ {
+ silent: true,
+ tag: 'read_notification',
+ }
+ );
+
+ res();
+
+ setTimeout(async () => {
+ for (const n of
+ [
+ ...(await self.registration.getNotifications({ tag: 'user_visible_auto_notification' })),
+ ...(await self.registration.getNotifications({ tag: 'read_notification' }))
+ ]
+ ) {
+ n.close();
+ }
+ }, 1000);
+ });
+}
diff --git a/packages/sw/src/scripts/get-account-from-id.ts b/packages/sw/src/scripts/get-account-from-id.ts
new file mode 100644
index 0000000000..be4cfaeba4
--- /dev/null
+++ b/packages/sw/src/scripts/get-account-from-id.ts
@@ -0,0 +1,7 @@
+import { get } from 'idb-keyval';
+
+export async function getAccountFromId(id: string) {
+ const accounts = await get('accounts') as { token: string; id: string; }[];
+ if (!accounts) console.log('Accounts are not recorded');
+ return accounts.find(e => e.id === id);
+}
diff --git a/packages/sw/src/scripts/get-user-name.ts b/packages/sw/src/scripts/get-user-name.ts
new file mode 100644
index 0000000000..d499ea0203
--- /dev/null
+++ b/packages/sw/src/scripts/get-user-name.ts
@@ -0,0 +1,3 @@
+export default function(user: { name?: string | null, username: string }): string {
+ return user.name || user.username;
+}
diff --git a/packages/sw/src/scripts/i18n.ts b/packages/sw/src/scripts/i18n.ts
new file mode 100644
index 0000000000..3fe88e5514
--- /dev/null
+++ b/packages/sw/src/scripts/i18n.ts
@@ -0,0 +1,29 @@
+export class I18n<T extends Record<string, any>> {
+ public ts: T;
+
+ constructor(locale: T) {
+ this.ts = locale;
+
+ //#region BIND
+ this.t = this.t.bind(this);
+ //#endregion
+ }
+
+ // string にしているのは、ドット区切りでのパス指定を許可するため
+ // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
+ public t(key: string, args?: Record<string, string>): string {
+ try {
+ let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string;
+
+ if (args) {
+ for (const [k, v] of Object.entries(args)) {
+ str = str.replace(`{${k}}`, v);
+ }
+ }
+ return str;
+ } catch (err) {
+ console.warn(`missing localization '${key}'`);
+ return key;
+ }
+ }
+}
diff --git a/packages/sw/src/scripts/lang.ts b/packages/sw/src/scripts/lang.ts
new file mode 100644
index 0000000000..2d05404ef9
--- /dev/null
+++ b/packages/sw/src/scripts/lang.ts
@@ -0,0 +1,47 @@
+/*
+ * Language manager for SW
+ */
+declare var self: ServiceWorkerGlobalScope;
+
+import { get, set } from 'idb-keyval';
+import { I18n } from '@/scripts/i18n';
+
+class SwLang {
+ public cacheName = `mk-cache-${_VERSION_}`;
+
+ public lang: Promise<string> = get('lang').then(async prelang => {
+ if (!prelang) return 'en-US';
+ return prelang;
+ });
+
+ public setLang(newLang: string) {
+ this.lang = Promise.resolve(newLang);
+ set('lang', newLang);
+ return this.fetchLocale();
+ }
+
+ public i18n: Promise<I18n<any>> | null = null;
+
+ public fetchLocale() {
+ return this.i18n = this._fetch();
+ }
+
+ private async _fetch() {
+ // Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う
+ const localeUrl = `/assets/locales/${await this.lang}.${_VERSION_}.json`;
+ let localeRes = await caches.match(localeUrl);
+
+ // _DEV_がtrueの場合は常に最新化
+ if (!localeRes || _DEV_) {
+ localeRes = await fetch(localeUrl);
+ const clone = localeRes?.clone();
+ if (!clone?.clone().ok) Error('locale fetching error');
+
+ caches.open(this.cacheName).then(cache => cache.put(localeUrl, clone));
+ }
+
+ return new I18n(await localeRes.json());
+ }
+}
+
+export const swLang = new SwLang();
diff --git a/packages/sw/src/scripts/login-id.ts b/packages/sw/src/scripts/login-id.ts
new file mode 100644
index 0000000000..0f9c6be4a9
--- /dev/null
+++ b/packages/sw/src/scripts/login-id.ts
@@ -0,0 +1,11 @@
+export function getUrlWithLoginId(url: string, loginId: string) {
+ const u = new URL(url, origin);
+ u.searchParams.append('loginId', loginId);
+ return u.toString();
+}
+
+export function getUrlWithoutLoginId(url: string) {
+ const u = new URL(url);
+ u.searchParams.delete('loginId');
+ return u.toString();
+}
diff --git a/packages/sw/src/scripts/notification-read.ts b/packages/sw/src/scripts/notification-read.ts
new file mode 100644
index 0000000000..8433f902b4
--- /dev/null
+++ b/packages/sw/src/scripts/notification-read.ts
@@ -0,0 +1,50 @@
+declare var self: ServiceWorkerGlobalScope;
+
+import { get } from 'idb-keyval';
+import { pushNotificationDataMap } from '@/types';
+import { api } from '@/scripts/operations';
+
+type Accounts = {
+ [x: string]: {
+ queue: string[],
+ timeout: number | null
+ }
+};
+
+class SwNotificationReadManager {
+ private accounts: Accounts = {};
+
+ public async construct() {
+ const accounts = await get('accounts');
+ if (!accounts) Error('Accounts are not recorded');
+
+ this.accounts = accounts.reduce((acc, e) => {
+ acc[e.id] = {
+ queue: [],
+ timeout: null
+ };
+ return acc;
+ }, {} as Accounts);
+
+ return this;
+ }
+
+ // プッシュ通知の既読をサーバーに送信
+ public async read<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]) {
+ if (data.type !== 'notification' || !(data.userId in this.accounts)) return;
+
+ const account = this.accounts[data.userId];
+
+ account.queue.push(data.body.id as string);
+
+ // 最後の呼び出しから200ms待ってまとめて処理する
+ if (account.timeout) clearTimeout(account.timeout);
+ account.timeout = setTimeout(() => {
+ account.timeout = null;
+
+ api('notifications/read', data.userId, { notificationIds: account.queue });
+ }, 200);
+ }
+}
+
+export const swNotificationRead = (new SwNotificationReadManager()).construct();
diff --git a/packages/sw/src/scripts/operations.ts b/packages/sw/src/scripts/operations.ts
new file mode 100644
index 0000000000..02cf0d96cf
--- /dev/null
+++ b/packages/sw/src/scripts/operations.ts
@@ -0,0 +1,70 @@
+/*
+ * Operations
+ * 各種操作
+ */
+declare var self: ServiceWorkerGlobalScope;
+
+import * as Misskey from 'misskey-js';
+import { SwMessage, swMessageOrderType } from '@/types';
+import { acct as getAcct } from '@/filters/user';
+import { getAccountFromId } from '@/scripts/get-account-from-id';
+import { getUrlWithLoginId } from '@/scripts/login-id';
+
+export const cli = new Misskey.api.APIClient({ origin, fetch: (...args) => fetch(...args) });
+
+export async function api<E extends keyof Misskey.Endpoints>(endpoint: E, userId: string, options?: Misskey.Endpoints[E]['req']) {
+ const account = await getAccountFromId(userId);
+ if (!account) return;
+
+ return cli.request(endpoint, options, account.token);
+}
+
+// rendered acctからユーザーを開く
+export function openUser(acct: string, loginId: string) {
+ return openClient('push', `/@${acct}`, loginId, { acct });
+}
+
+// noteIdからノートを開く
+export function openNote(noteId: string, loginId: string) {
+ return openClient('push', `/notes/${noteId}`, loginId, { noteId });
+}
+
+export async function openChat(body: any, loginId: string) {
+ if (body.groupId === null) {
+ return openClient('push', `/my/messaging/${getAcct(body.user)}`, loginId, { body });
+ } else {
+ return openClient('push', `/my/messaging/group/${body.groupId}`, loginId, { body });
+ }
+}
+
+// post-formのオプションから投稿フォームを開く
+export async function openPost(options: any, loginId: string) {
+ // クエリを作成しておく
+ let url = `/share?`;
+ if (options.initialText) url += `text=${options.initialText}&`;
+ if (options.reply) url += `replyId=${options.reply.id}&`;
+ if (options.renote) url += `renoteId=${options.renote.id}&`;
+
+ return openClient('post', url, loginId, { options });
+}
+
+export async function openClient(order: swMessageOrderType, url: string, loginId: string, query: any = {}) {
+ const client = await findClient();
+
+ if (client) {
+ client.postMessage({ type: 'order', ...query, order, loginId, url } as SwMessage);
+ return client;
+ }
+
+ return self.clients.openWindow(getUrlWithLoginId(url, loginId));
+}
+
+export async function findClient() {
+ const clients = await self.clients.matchAll({
+ type: 'window'
+ });
+ for (const c of clients) {
+ if (c.url.indexOf('?zen') < 0) return c;
+ }
+ return null;
+}
diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts
new file mode 100644
index 0000000000..0ba6a6e4af
--- /dev/null
+++ b/packages/sw/src/sw.ts
@@ -0,0 +1,200 @@
+declare var self: ServiceWorkerGlobalScope;
+
+import { createEmptyNotification, createNotification } from '@/scripts/create-notification';
+import { swLang } from '@/scripts/lang';
+import { swNotificationRead } from '@/scripts/notification-read';
+import { pushNotificationDataMap } from '@/types';
+import * as swos from '@/scripts/operations';
+import { acct as getAcct } from '@/filters/user';
+
+self.addEventListener('install', ev => {
+ ev.waitUntil(self.skipWaiting());
+});
+
+self.addEventListener('activate', ev => {
+ ev.waitUntil(
+ caches.keys()
+ .then(cacheNames => Promise.all(
+ cacheNames
+ .filter((v) => v !== swLang.cacheName)
+ .map(name => caches.delete(name))
+ ))
+ .then(() => self.clients.claim())
+ );
+});
+
+self.addEventListener('fetch', ev => {
+ ev.respondWith(
+ fetch(ev.request)
+ .catch(() => new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 }))
+ );
+});
+
+self.addEventListener('push', ev => {
+ // クライアント取得
+ ev.waitUntil(self.clients.matchAll({
+ includeUncontrolled: true,
+ type: 'window'
+ }).then(async <K extends keyof pushNotificationDataMap>(clients: readonly WindowClient[]) => {
+ const data: pushNotificationDataMap[K] = ev.data?.json();
+
+ switch (data.type) {
+ // case 'driveFileCreated':
+ case 'notification':
+ case 'unreadMessagingMessage':
+ // クライアントがあったらストリームに接続しているということなので通知しない
+ if (clients.length != 0) return;
+ return createNotification(data);
+ case 'readAllNotifications':
+ for (const n of await self.registration.getNotifications()) {
+ if (n?.data?.type === 'notification') n.close();
+ }
+ break;
+ case 'readAllMessagingMessages':
+ for (const n of await self.registration.getNotifications()) {
+ if (n?.data?.type === 'unreadMessagingMessage') n.close();
+ }
+ break;
+ case 'readNotifications':
+ for (const n of await self.registration.getNotifications()) {
+ if (data.body?.notificationIds?.includes(n.data.body.id)) {
+ n.close();
+ }
+ }
+ break;
+ case 'readAllMessagingMessagesOfARoom':
+ for (const n of await self.registration.getNotifications()) {
+ if (n.data.type === 'unreadMessagingMessage'
+ && ('userId' in data.body
+ ? data.body.userId === n.data.body.userId
+ : data.body.groupId === n.data.body.groupId)
+ ) {
+ n.close();
+ }
+ }
+ break;
+ }
+
+ return createEmptyNotification();
+ }));
+});
+
+self.addEventListener('notificationclick', <K extends keyof pushNotificationDataMap>(ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => {
+ ev.waitUntil((async () => {
+ if (_DEV_) {
+ console.log('notificationclick', ev.action, ev.notification.data);
+ }
+
+ const { action, notification } = ev;
+ const data: pushNotificationDataMap[K] = notification.data;
+ const { userId: id } = data;
+ let client: WindowClient | null = null;
+
+ switch (data.type) {
+ case 'notification':
+ switch (action) {
+ case 'follow':
+ if ('userId' in data.body) await swos.api('following/create', id, { userId: data.body.userId });
+ break;
+ case 'showUser':
+ if ('user' in data.body) client = await swos.openUser(getAcct(data.body.user), id);
+ break;
+ case 'reply':
+ if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, id);
+ break;
+ case 'renote':
+ if ('note' in data.body) await swos.api('notes/create', id, { renoteId: data.body.note.id });
+ break;
+ case 'accept':
+ switch (data.body.type) {
+ case 'receiveFollowRequest':
+ await swos.api('following/requests/accept', id, { userId: data.body.userId });
+ break;
+ case 'groupInvited':
+ await swos.api('users/groups/invitations/accept', id, { invitationId: data.body.invitation.id });
+ break;
+ }
+ break;
+ case 'reject':
+ switch (data.body.type) {
+ case 'receiveFollowRequest':
+ await swos.api('following/requests/reject', id, { userId: data.body.userId });
+ break;
+ case 'groupInvited':
+ await swos.api('users/groups/invitations/reject', id, { invitationId: data.body.invitation.id });
+ break;
+ }
+ break;
+ case 'showFollowRequests':
+ client = await swos.openClient('push', '/my/follow-requests', id);
+ break;
+ default:
+ switch (data.body.type) {
+ case 'receiveFollowRequest':
+ client = await swos.openClient('push', '/my/follow-requests', id);
+ break;
+ case 'groupInvited':
+ client = await swos.openClient('push', '/my/groups', id);
+ break;
+ case 'reaction':
+ client = await swos.openNote(data.body.note.id, id);
+ break;
+ default:
+ if ('note' in data.body) {
+ client = await swos.openNote(data.body.note.id, id);
+ } else if ('user' in data.body) {
+ client = await swos.openUser(getAcct(data.body.user), id);
+ }
+ break;
+ }
+ }
+ break;
+ case 'unreadMessagingMessage':
+ client = await swos.openChat(data.body, id);
+ break;
+ }
+
+ if (client) {
+ client.focus();
+ }
+ if (data.type === 'notification') {
+ swNotificationRead.then(that => that.read(data));
+ }
+
+ notification.close();
+
+ })());
+});
+
+self.addEventListener('notificationclose', <K extends keyof pushNotificationDataMap>(ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => {
+ const data: pushNotificationDataMap[K] = ev.notification.data;
+
+ if (data.type === 'notification') {
+ swNotificationRead.then(that => that.read(data));
+ }
+});
+
+self.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => {
+ ev.waitUntil((async () => {
+ switch (ev.data) {
+ case 'clear':
+ // Cache Storage全削除
+ await caches.keys()
+ .then(cacheNames => Promise.all(
+ cacheNames.map(name => caches.delete(name))
+ ));
+ return; // TODO
+ }
+
+ if (typeof ev.data === 'object') {
+ // E.g. '[object Array]' → 'array'
+ const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase();
+
+ if (otype === 'object') {
+ if (ev.data.msg === 'initialize') {
+ swLang.setLang(ev.data.lang);
+ }
+ }
+ }
+ })());
+});
diff --git a/packages/sw/src/types.ts b/packages/sw/src/types.ts
new file mode 100644
index 0000000000..6aa3726eac
--- /dev/null
+++ b/packages/sw/src/types.ts
@@ -0,0 +1,31 @@
+import * as Misskey from 'misskey-js';
+
+export type swMessageOrderType = 'post' | 'push';
+
+export type SwMessage = {
+ type: 'order';
+ order: swMessageOrderType;
+ loginId: string;
+ url: string;
+ [x: string]: any;
+};
+
+// Defined also @/services/push-notification.ts#L7-L14
+type pushNotificationDataSourceMap = {
+ notification: Misskey.entities.Notification;
+ unreadMessagingMessage: Misskey.entities.MessagingMessage;
+ readNotifications: { notificationIds: string[] };
+ readAllNotifications: undefined;
+ readAllMessagingMessages: undefined;
+ readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string };
+};
+
+export type pushNotificationData<K extends keyof pushNotificationDataSourceMap> = {
+ type: K;
+ body: pushNotificationDataSourceMap[K];
+ userId: string;
+};
+
+export type pushNotificationDataMap = {
+ [K in keyof pushNotificationDataSourceMap]: pushNotificationData<K>;
+};