From dbb9c3d248710b74e6ac9b2c17cad858183c3331 Mon Sep 17 00:00:00 2001 From: okayurisotto Date: Wed, 12 Apr 2023 01:07:24 +0900 Subject: [PATCH] Refactor sw (#10579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(sw): remove dead code * refactor(sw): remove dead code * refactor(sw): remove dead code * refactor(sw): remove dead code * refactor(sw): remove dead code * refactor(sw): remove dead code * refactor(sw): 冗長な部分を変更 * refactor(sw): 使われていない煩雑な機能を削除 * refactor(sw): remove dead code * refactor(sw): URL文字列の作成に`URL`を使うように * refactor(sw): 型アサーションの削除とそれに伴い露呈したエラーへの対処 * refactor(sw): `append` -> `set` in `URLSearchParams` * refactor(sw): `any`の削除とそれに伴い露呈したエラーへの対処 * refactor(sw): 型アサーションの削除とそれに伴い露呈したエラーへの対処 対処と言っても`throw`するだけ。いままでもこの状況ではエラーが投げられていたはずなので、この対処により新たな問題が起きることはないはず。 * refactor(sw): i18n loading * refactor(sw): 型推論がうまくできる書き方に変更 `codes`が`(string | undefined)[]`から`string[]`になった * refactor(sw): クエリ文字列の作成に`URLSearchParams`を使うように * refactor(sw): `findClient` * refactor(sw): `openClient`における`any`や`as`の書き換え * refactor(sw): `openPost`における`any`の書き換え * refactor(sw): `let` -> `const` * refactor(sw): `any` -> `unknown` * cleanup(sw): import * cleanup(sw) * cleanup(sw): `?.` * cleanup(sw/.eslintrc.js) * refactor(sw): `@typescript-eslint/explicit-function-return-type` * refactor(sw): `@typescript-eslint/no-unused-vars` * refactor(sw): どうしようもないところに`eslint-disable-next-line`を * refactor(sw): `import/no-default-export` * update operations.ts * throw new Error --------- Co-authored-by: tamaina Co-authored-by: Kainoa kanter --- packages/sw/src/@types/global.d.ts | 8 + packages/sw/src/filters/user.ts | 14 -- .../sw/src/scripts/create-notification.ts | 85 +++---- .../sw/src/scripts/get-account-from-id.ts | 11 +- packages/sw/src/scripts/get-user-name.ts | 4 +- packages/sw/src/scripts/i18n.ts | 7 +- packages/sw/src/scripts/lang.ts | 20 +- packages/sw/src/scripts/login-id.ts | 10 +- packages/sw/src/scripts/operations.ts | 112 +++++---- packages/sw/src/scripts/twemoji-base.ts | 12 +- packages/sw/src/scripts/url.ts | 15 -- packages/sw/src/sw.ts | 224 +++++++++--------- packages/sw/src/types.ts | 41 +++- 13 files changed, 291 insertions(+), 272 deletions(-) create mode 100644 packages/sw/src/@types/global.d.ts delete mode 100644 packages/sw/src/filters/user.ts delete mode 100644 packages/sw/src/scripts/url.ts diff --git a/packages/sw/src/@types/global.d.ts b/packages/sw/src/@types/global.d.ts new file mode 100644 index 000000000..a7d176b0b --- /dev/null +++ b/packages/sw/src/@types/global.d.ts @@ -0,0 +1,8 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type FIXME = any; + +declare const _LANGS_: string[][]; +declare const _VERSION_: string; +declare const _ENV_: string; +declare const _DEV_: boolean; +declare const _PERF_PREFIX_: string; diff --git a/packages/sw/src/filters/user.ts b/packages/sw/src/filters/user.ts deleted file mode 100644 index 782194d16..000000000 --- a/packages/sw/src/filters/user.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as misskey from "calckey-js"; -import * as Acct from "calckey-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 index 03ce2a60d..12d76aabf 100644 --- a/packages/sw/src/scripts/create-notification.ts +++ b/packages/sw/src/scripts/create-notification.ts @@ -1,23 +1,36 @@ /* * Notification manager for SW */ -declare let 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"; +import type { BadgeNames, PushNotificationDataMap } from "@/types"; import { char2fileName } from "@/scripts/twemoji-base"; -import * as url from "@/scripts/url"; +import { cli } from "@/scripts/operations"; +import { getAccountFromId } from "@/scripts/get-account-from-id"; +import { swLang } from "@/scripts/lang"; +import { getUserName } from "@/scripts/get-user-name"; -const iconUrl = (name: string) => - `/static-assets/notification-badges/${name}.png`; +const closeNotificationsByTags = async (tags: string[]): Promise => { + for (const n of ( + await Promise.all( + tags.map((tag) => globalThis.registration.getNotifications({ tag })), + ) + ).flat()) { + n.close(); + } +}; + +const iconUrl = (name: BadgeNames): string => + `/static-assets/tabler-badges/${name}.png`; +/* How to add a new badge: + * 1. Find the icon and download png from https://tabler-icons.io/ + * 2. vips resize ~/Downloads/icon-name.png vipswork.png 0.4; vips scRGB2BW vipswork.png ~/icon-name.png"[compression=9,strip]"; rm vipswork.png; + * 3. mv ~/icon-name.png ~/misskey/packages/backend/assets/tabler-badges/ + * 4. Add 'icon-name' to BadgeNames + * 5. Add `badge: iconUrl('icon-name'),` + */ export async function createNotification< - K extends keyof pushNotificationDataMap, ->(data: pushNotificationDataMap[K]) { + K extends keyof PushNotificationDataMap, +>(data: PushNotificationDataMap[K]): Promise { const n = await composeNotification(data); if (n) { @@ -28,11 +41,10 @@ export async function createNotification< } } -async function composeNotification( - data: pushNotificationDataMap[K], +async function composeNotification( + data: PushNotificationDataMap[keyof PushNotificationDataMap], ): Promise<[string, NotificationOptions] | null> { - if (!swLang.i18n) swLang.fetchLocale(); - const i18n = (await swLang.i18n) as I18n; + const i18n = await (swLang.i18n ?? swLang.fetchLocale()); const { t } = i18n; switch (data.type) { /* @@ -164,38 +176,20 @@ async function composeNotification( if (reaction.startsWith(":")) { // カスタム絵文字の場合 - const customEmoji = data.body.note.emojis.find( - (x) => x.name === reaction.substr(1, reaction.length - 2), - ); - if (customEmoji) { - if (reaction.includes("@")) { - reaction = `:${reaction.substr(1, reaction.indexOf("@") - 1)}:`; - } - - const u = new URL(customEmoji.url); - if (u.href.startsWith(`${origin}/proxy/`)) { - // もう既にproxyっぽそうだったらsearchParams付けるだけ - u.searchParams.set("badge", "1"); - badge = u.href; - } else { - const dummy = `${u.host}${u.pathname}`; // 拡張子がないとキャッシュしてくれないCDNがあるので - badge = `${origin}/proxy/${dummy}?${url.query({ - url: u.href, - badge: "1", - })}`; - } - } + const name = reaction.substring(1, reaction.length - 1); + const badgeUrl = new URL(`/emoji/${name}.webp`, origin); + badgeUrl.searchParams.set("badge", "1"); + badge = badgeUrl.href; + reaction = name.split("@")[0]; } else { // Unicode絵文字の場合 badge = `/twemoji-badge/${char2fileName(reaction)}.png`; } if ( - badge - ? await fetch(badge) - .then((res) => res.status !== 200) - .catch(() => true) - : true + await fetch(badge) + .then((res) => res.status !== 200) + .catch(() => true) ) { badge = iconUrl("plus"); } @@ -339,10 +333,9 @@ async function composeNotification( } } -export async function createEmptyNotification() { +export async function createEmptyNotification(): Promise { return new Promise(async (res) => { - if (!swLang.i18n) swLang.fetchLocale(); - const i18n = (await swLang.i18n) as I18n; + const i18n = await (swLang.i18n ?? swLang.fetchLocale()); const { t } = i18n; await self.registration.showNotification( diff --git a/packages/sw/src/scripts/get-account-from-id.ts b/packages/sw/src/scripts/get-account-from-id.ts index 24f5e590e..dfcb08d0e 100644 --- a/packages/sw/src/scripts/get-account-from-id.ts +++ b/packages/sw/src/scripts/get-account-from-id.ts @@ -1,7 +1,12 @@ 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"); +export async function getAccountFromId( + id: string, +): Promise<{ token: string; id: string } | void> { + const accounts = await get<{ token: string; id: string }[]>("accounts"); + if (!accounts) { + console.log("Accounts are not recorded"); + return; + } 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 index 0f8916de8..b0a87d9b8 100644 --- a/packages/sw/src/scripts/get-user-name.ts +++ b/packages/sw/src/scripts/get-user-name.ts @@ -1,6 +1,6 @@ -export default function (user: { +export function getUserName(user: { name?: string | null; username: string; }): string { - return user.name || user.username; + return user.name === "" ? user.username : user.name ?? user.username; } diff --git a/packages/sw/src/scripts/i18n.ts b/packages/sw/src/scripts/i18n.ts index 06fe83f31..71ac9e7b0 100644 --- a/packages/sw/src/scripts/i18n.ts +++ b/packages/sw/src/scripts/i18n.ts @@ -1,4 +1,6 @@ -export class I18n> { +export type Locale = { [key: string]: string | Locale }; + +export class I18n { public ts: T; constructor(locale: T) { @@ -15,7 +17,8 @@ export class I18n> { try { let str = key .split(".") - .reduce((o, i) => o[i], this.ts) as unknown as string; + .reduce((o, i) => o[i], this.ts); + if (typeof str !== "string") throw new Error(); if (args) { for (const [k, v] of Object.entries(args)) { diff --git a/packages/sw/src/scripts/lang.ts b/packages/sw/src/scripts/lang.ts index 31cfb7bd3..bdfc25ef6 100644 --- a/packages/sw/src/scripts/lang.ts +++ b/packages/sw/src/scripts/lang.ts @@ -1,10 +1,8 @@ /* * Language manager for SW */ -declare let self: ServiceWorkerGlobalScope; - import { get, set } from "idb-keyval"; -import { I18n } from "@/scripts/i18n"; +import { I18n, type Locale } from "@/scripts/i18n"; class SwLang { public cacheName = `mk-cache-${_VERSION_}`; @@ -14,19 +12,19 @@ class SwLang { return prelang; }); - public setLang(newLang: string) { + public setLang(newLang: string): Promise> { this.lang = Promise.resolve(newLang); set("lang", newLang); return this.fetchLocale(); } - public i18n: Promise> | null = null; + public i18n: Promise | null = null; - public fetchLocale() { - return this.i18n === this._fetch(); + public fetchLocale(): Promise> { + return (this.i18n = this._fetch()); } - private async _fetch() { + private async _fetch(): Promise> { // Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う const localeUrl = `/assets/locales/${await this.lang}.${_VERSION_}.json`; let localeRes = await caches.match(localeUrl); @@ -34,13 +32,13 @@ class SwLang { // _DEV_がtrueの場合は常に最新化 if (!localeRes || _DEV_) { localeRes = await fetch(localeUrl); - const clone = localeRes?.clone(); - if (!clone?.clone().ok) Error("locale fetching error"); + const clone = localeRes.clone(); + if (!clone.clone().ok) throw new Error("locale fetching error"); caches.open(this.cacheName).then((cache) => cache.put(localeUrl, clone)); } - return new I18n(await localeRes.json()); + return new I18n(await localeRes.json()); } } diff --git a/packages/sw/src/scripts/login-id.ts b/packages/sw/src/scripts/login-id.ts index 0fe345780..773b01536 100644 --- a/packages/sw/src/scripts/login-id.ts +++ b/packages/sw/src/scripts/login-id.ts @@ -1,11 +1,5 @@ -export function getUrlWithLoginId(url: string, loginId: string) { +export function getUrlWithLoginId(url: string, loginId: string): 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"); + u.searchParams.set("loginId", loginId); return u.toString(); } diff --git a/packages/sw/src/scripts/operations.ts b/packages/sw/src/scripts/operations.ts index ae76d6ecf..74d1b053e 100644 --- a/packages/sw/src/scripts/operations.ts +++ b/packages/sw/src/scripts/operations.ts @@ -2,69 +2,100 @@ * Operations * 各種操作 */ -declare let self: ServiceWorkerGlobalScope; - import * as Misskey from "calckey-js"; -import { SwMessage, swMessageOrderType } from "@/types"; -import { acct as getAcct } from "@/filters/user"; +import type { SwMessage, SwMessageOrderType } from "@/types"; 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), + fetch: (...args): Promise => fetch(...args), }); -export async function api( +export async function api< + E extends keyof Misskey.Endpoints, + O extends Misskey.Endpoints[E]["req"], +>( endpoint: E, - userId: string, - options?: Misskey.Endpoints[E]["req"], -) { - const account = await getAccountFromId(userId); - if (!account) return; + userId?: string, + options?: O, +): Promise>> { + let account: { token: string; id: string } | void; - return cli.request(endpoint, options, account.token); + if (userId) { + account = await getAccountFromId(userId); + if (!account) return; + } + + return cli.request(endpoint, options, account?.token); +} + +// mark-all-as-read送出を1秒間隔に制限する +const readBlockingStatus = new Map(); +export function sendMarkAllAsRead( + userId: string, +): Promise { + if (readBlockingStatus.get(userId)) return Promise.resolve(); + readBlockingStatus.set(userId, true); + return new Promise((resolve) => { + setTimeout(() => { + readBlockingStatus.set(userId, false); + api("notifications/mark-all-as-read", userId).then(resolve, resolve); + }, 1000); + }); } // rendered acctからユーザーを開く -export function openUser(acct: string, loginId: string) { +export function openUser( + acct: string, + loginId?: string, +): ReturnType { return openClient("push", `/@${acct}`, loginId, { acct }); } // noteIdからノートを開く -export function openNote(noteId: string, loginId: string) { +export function openNote( + noteId: string, + loginId?: string, +): ReturnType { 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, - }); - } +// noteIdからノートを開く +export function openAntenna( + antennaId: string, + loginId: string, +): ReturnType { + return openClient("push", `/timeline/antenna/${antennaId}`, loginId, { + antennaId, + }); } // post-formのオプションから投稿フォームを開く -export async function openPost(options: any, loginId: string) { +export async function openPost( + options: { + initialText?: string; + reply?: Misskey.entities.Note; + renote?: Misskey.entities.Note; + }, + loginId?: string, +): ReturnType { // クエリを作成しておく - 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}&`; + const url = "/share"; + const query = new URLSearchParams(); + if (options.initialText) query.set("text", options.initialText); + if (options.reply) query.set("replyId", options.reply.id); + if (options.renote) query.set("renoteId", options.renote.id); - return openClient("post", url, loginId, { options }); + return openClient("post", `${url}?${query}`, loginId, { options }); } export async function openClient( - order: swMessageOrderType, + order: SwMessageOrderType, url: string, - loginId: string, - query: any = {}, -) { + loginId?: string, + query: Record = {}, +): Promise { const client = await findClient(); if (client) { @@ -74,19 +105,16 @@ export async function openClient( order, loginId, url, - } as SwMessage); + } satisfies SwMessage); return client; } - return self.clients.openWindow(getUrlWithLoginId(url, loginId)); + return self.clients.openWindow(getUrlWithLoginId(url, loginId!)); } -export async function findClient() { - const clients = await self.clients.matchAll({ +export async function findClient(): Promise { + const clients = await globalThis.clients.matchAll({ type: "window", }); - for (const c of clients) { - if (c.url.indexOf("?zen") < 0) return c; - } - return null; + return clients.find((c) => !new URL(c.url).searchParams.has("zen")) ?? null; } diff --git a/packages/sw/src/scripts/twemoji-base.ts b/packages/sw/src/scripts/twemoji-base.ts index 0e45a527e..ca35befdc 100644 --- a/packages/sw/src/scripts/twemoji-base.ts +++ b/packages/sw/src/scripts/twemoji-base.ts @@ -1,12 +1,8 @@ -export const twemojiSvgBase = "/twemoji"; - export function char2fileName(char: string): string { - let codes = Array.from(char).map((x) => x.codePointAt(0)?.toString(16)); + let codes = Array.from(char) + .map((x) => x.codePointAt(0)?.toString(16)) + .filter((x: T | undefined): x is T => x !== undefined); if (!codes.includes("200d")) codes = codes.filter((x) => x !== "fe0f"); - codes = codes.filter((x) => x?.length); + codes = codes.filter((x) => x.length !== 0); return codes.join("-"); } - -export function char2filePath(char: string): string { - return `${twemojiSvgBase}/${char2fileName(char)}.svg`; -} diff --git a/packages/sw/src/scripts/url.ts b/packages/sw/src/scripts/url.ts deleted file mode 100644 index a11f5e28b..000000000 --- a/packages/sw/src/scripts/url.ts +++ /dev/null @@ -1,15 +0,0 @@ -export function query(obj: {}): string { - const params = Object.entries(obj) - .filter(([, v]) => (Array.isArray(v) ? v.length : v !== undefined)) - .reduce((a, [k, v]) => ((a[k] = v), a), {} as Record); - - return Object.entries(params) - .map((e) => `${e[0]}=${encodeURIComponent(e[1])}`) - .join("&"); -} - -export function appendQuery(url: string, query: string): string { - return `${url}${ - /\?/.test(url) ? (url.endsWith("?") ? "" : "&") : "?" - }${query}`; -} diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index 74603d67c..6a2ba3bf1 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -1,20 +1,18 @@ -declare let self: ServiceWorkerGlobalScope; - +import { get } from "idb-keyval"; +import * as Acct from "calckey-js/built/acct"; +import type { PushNotificationDataMap } from "@/types"; 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()); +globalThis.addEventListener("install", () => { + // ev.waitUntil(globalThis.skipWaiting()); }); -self.addEventListener("activate", (ev) => { +globalThis.addEventListener("activate", (ev) => { ev.waitUntil( caches .keys() @@ -25,11 +23,15 @@ self.addEventListener("activate", (ev) => { .map((name) => caches.delete(name)), ), ) - .then(() => self.clients.claim()), + .then(() => globalThis.clients.claim()), ); }); -self.addEventListener("fetch", (ev) => { +function offlineContentHTML(): string { + return `Offline. Service Worker @${_VERSION_} `; +} + +globalThis.addEventListener("fetch", (ev) => { let isHTMLRequest = false; if (ev.request.headers.get("sec-fetch-dest") === "document") { isHTMLRequest = true; @@ -41,90 +43,68 @@ self.addEventListener("fetch", (ev) => { if (!isHTMLRequest) return; ev.respondWith( - fetch(ev.request).catch( - () => - new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 }), - ), + fetch(ev.request).catch(() => { + return new Response(offlineContentHTML(), { + status: 200, + headers: { + "content-type": "text/html", + }, + }); + }), ); }); -self.addEventListener("push", (ev) => { +globalThis.addEventListener("push", (ev) => { // クライアント取得 ev.waitUntil( - self.clients + globalThis.clients .matchAll({ includeUncontrolled: true, type: "window", }) - .then( - async ( - clients: readonly WindowClient[], - ) => { - const data: pushNotificationDataMap[K] = ev.data?.json(); + .then(async () => { + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = + ev.data?.json(); - switch (data.type) { - // case 'driveFileCreated': - case "notification": - case "unreadMessagingMessage": - // 1日以上経過している場合は無視 - if (new Date().getTime() - data.dateTime > 1000 * 60 * 60 * 24) - break; + switch (data.type) { + // case 'driveFileCreated': + case "notification": + case "unreadAntennaNote": + // 1日以上経過している場合は無視 + if (new Date().getTime() - data.dateTime > 1000 * 60 * 60 * 24) + break; - // クライアントがあったらストリームに接続しているということなので通知しない - if (clients.length !== 0) break; + return createNotification(data); + case "readAllNotifications": + await globalThis.registration + .getNotifications() + .then((notifications) => + notifications.forEach( + (n) => n.tag !== "read_notification" && n.close(), + ), + ); + break; + } - 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(); - }, - ), + await createEmptyNotification(); + return; + }), ); }); -self.addEventListener( +(globalThis as unknown as ServiceWorkerGlobalScope).addEventListener( "notificationclick", - ( - ev: ServiceWorkerGlobalScopeEventMap["notificationclick"], - ) => { + (ev: ServiceWorkerGlobalScopeEventMap["notificationclick"]) => { ev.waitUntil( - (async () => { + (async (): Promise => { if (_DEV_) { console.log("notificationclick", ev.action, ev.notification.data); } const { action, notification } = ev; - const data: pushNotificationDataMap[K] = notification.data; - const { userId: id } = data; + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = + notification.data ?? {}; + const { userId: loginId } = data; let client: WindowClient | null = null; switch (data.type) { @@ -132,57 +112,53 @@ self.addEventListener( switch (action) { case "follow": if ("userId" in data.body) - await swos.api("following/create", id, { + await swos.api("following/create", loginId, { userId: data.body.userId, }); break; case "showUser": if ("user" in data.body) - client = await swos.openUser(getAcct(data.body.user), id); + client = await swos.openUser( + Acct.toString(data.body.user), + loginId, + ); break; case "reply": if ("note" in data.body) - client = await swos.openPost({ reply: data.body.note }, id); + client = await swos.openPost( + { reply: data.body.note }, + loginId, + ); break; case "renote": if ("note" in data.body) - await swos.api("notes/create", id, { + await swos.api("notes/create", loginId, { renoteId: data.body.note.id, }); break; case "accept": switch (data.body.type) { case "receiveFollowRequest": - await swos.api("following/requests/accept", id, { + await swos.api("following/requests/accept", loginId, { 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, { + await swos.api("following/requests/reject", loginId, { 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, + loginId, ); break; default: @@ -191,35 +167,61 @@ self.addEventListener( client = await swos.openClient( "push", "/my/follow-requests", - id, + loginId, ); break; - case "groupInvited": - client = await swos.openClient("push", "/my/groups", id); - break; case "reaction": - client = await swos.openNote(data.body.note.id, id); + client = await swos.openNote(data.body.note.id, loginId); break; default: if ("note" in data.body) { - client = await swos.openNote(data.body.note.id, id); + client = await swos.openNote(data.body.note.id, loginId); } else if ("user" in data.body) { - client = await swos.openUser(getAcct(data.body.user), id); + client = await swos.openUser( + Acct.toString(data.body.user), + loginId, + ); } break; } } break; - case "unreadMessagingMessage": - client = await swos.openChat(data.body, id); + case "unreadAntennaNote": + client = await swos.openAntenna(data.body.antenna.id, loginId); break; + default: + switch (action) { + case "markAllAsRead": + await globalThis.registration + .getNotifications() + .then((notifications) => + notifications.forEach( + (n) => n.tag !== "read_notification" && n.close(), + ), + ); + await get("accounts").then((accounts) => { + return Promise.all( + accounts.map(async (account) => { + await swos.sendMarkAllAsRead(account.id); + }), + ); + }); + break; + case "settings": + client = await swos.openClient( + "push", + "/settings/notifications", + loginId, + ); + break; + } } if (client) { client.focus(); } if (data.type === "notification") { - swNotificationRead.then((that) => that.read(data)); + await swos.sendMarkAllAsRead(loginId); } notification.close(); @@ -228,24 +230,28 @@ self.addEventListener( }, ); -self.addEventListener( +(globalThis as unknown as ServiceWorkerGlobalScope).addEventListener( "notificationclose", - ( - ev: ServiceWorkerGlobalScopeEventMap["notificationclose"], - ) => { - const data: pushNotificationDataMap[K] = ev.notification.data; + (ev: ServiceWorkerGlobalScopeEventMap["notificationclose"]) => { + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = + ev.notification.data; - if (data.type === "notification") { - swNotificationRead.then((that) => that.read(data)); - } + ev.waitUntil( + (async (): Promise => { + if (data.type === "notification") { + await swos.sendMarkAllAsRead(data.userId); + } + return; + })(), + ); }, ); -self.addEventListener( +(globalThis as unknown as ServiceWorkerGlobalScope).addEventListener( "message", (ev: ServiceWorkerGlobalScopeEventMap["message"]) => { ev.waitUntil( - (async () => { + (async (): Promise => { switch (ev.data) { case "clear": // Cache Storage全削除 diff --git a/packages/sw/src/types.ts b/packages/sw/src/types.ts index 984076dbe..f66362565 100644 --- a/packages/sw/src/types.ts +++ b/packages/sw/src/types.ts @@ -1,34 +1,51 @@ import * as Misskey from "calckey-js"; -export type swMessageOrderType = "post" | "push"; +export type SwMessageOrderType = "post" | "push"; export type SwMessage = { type: "order"; - order: swMessageOrderType; - loginId: string; + order: SwMessageOrderType; + loginId?: string; url: string; - [x: string]: any; + [x: string]: unknown; }; // Defined also @/services/push-notification.ts#L7-L14 -type pushNotificationDataSourceMap = { +type PushNotificationDataSourceMap = { notification: Misskey.entities.Notification; - unreadMessagingMessage: Misskey.entities.MessagingMessage; - readNotifications: { notificationIds: string[] }; + unreadAntennaNote: { + antenna: { id: string; name: string }; + note: Misskey.entities.Note; + }; readAllNotifications: undefined; readAllMessagingMessages: undefined; readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string }; }; -export type pushNotificationData< - K extends keyof pushNotificationDataSourceMap, +export type PushNotificationData< + K extends keyof PushNotificationDataSourceMap, > = { type: K; - body: pushNotificationDataSourceMap[K]; + body: PushNotificationDataSourceMap[K]; userId: string; dateTime: number; }; -export type pushNotificationDataMap = { - [K in keyof pushNotificationDataSourceMap]: pushNotificationData; +export type PushNotificationDataMap = { + [K in keyof PushNotificationDataSourceMap]: PushNotificationData; }; + +export type BadgeNames = + | "null" + | "antenna" + | "arrow-back-up" + | "at" + | "chart-arrows" + | "circle-check" + | "medal" + | "messages" + | "plus" + | "quote" + | "repeat" + | "user-plus" + | "users";