Refactor sw (#10579)

* 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 <tamaina@hotmail.co.jp>
Co-authored-by: Kainoa kanter <kainoa@t1c.dev>
This commit is contained in:
okayurisotto 2023-04-12 01:07:24 +09:00 committed by ThatOneCalculator
parent 479d76d763
commit 599417de6e
No known key found for this signature in database
GPG key ID: 8703CACD01000000
13 changed files with 291 additions and 272 deletions

8
packages/sw/src/@types/global.d.ts vendored Normal file
View file

@ -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;

View file

@ -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}` : ""}`;
};

View file

@ -1,23 +1,36 @@
/* /*
* Notification manager for SW * Notification manager for SW
*/ */
declare let self: ServiceWorkerGlobalScope; import type { BadgeNames, PushNotificationDataMap } from "@/types";
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 { char2fileName } from "@/scripts/twemoji-base"; 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) => const closeNotificationsByTags = async (tags: string[]): Promise<void> => {
`/static-assets/notification-badges/${name}.png`; 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< export async function createNotification<
K extends keyof pushNotificationDataMap, K extends keyof PushNotificationDataMap,
>(data: pushNotificationDataMap[K]) { >(data: PushNotificationDataMap[K]): Promise<void> {
const n = await composeNotification(data); const n = await composeNotification(data);
if (n) { if (n) {
@ -28,11 +41,10 @@ export async function createNotification<
} }
} }
async function composeNotification<K extends keyof pushNotificationDataMap>( async function composeNotification(
data: pushNotificationDataMap[K], data: PushNotificationDataMap[keyof PushNotificationDataMap],
): Promise<[string, NotificationOptions] | null> { ): Promise<[string, NotificationOptions] | null> {
if (!swLang.i18n) swLang.fetchLocale(); const i18n = await (swLang.i18n ?? swLang.fetchLocale());
const i18n = (await swLang.i18n) as I18n<any>;
const { t } = i18n; const { t } = i18n;
switch (data.type) { switch (data.type) {
/* /*
@ -164,38 +176,20 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(
if (reaction.startsWith(":")) { if (reaction.startsWith(":")) {
// カスタム絵文字の場合 // カスタム絵文字の場合
const customEmoji = data.body.note.emojis.find( const name = reaction.substring(1, reaction.length - 1);
(x) => x.name === reaction.substr(1, reaction.length - 2), const badgeUrl = new URL(`/emoji/${name}.webp`, origin);
); badgeUrl.searchParams.set("badge", "1");
if (customEmoji) { badge = badgeUrl.href;
if (reaction.includes("@")) { reaction = name.split("@")[0];
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",
})}`;
}
}
} else { } else {
// Unicode絵文字の場合 // Unicode絵文字の場合
badge = `/twemoji-badge/${char2fileName(reaction)}.png`; badge = `/twemoji-badge/${char2fileName(reaction)}.png`;
} }
if ( if (
badge await fetch(badge)
? await fetch(badge)
.then((res) => res.status !== 200) .then((res) => res.status !== 200)
.catch(() => true) .catch(() => true)
: true
) { ) {
badge = iconUrl("plus"); badge = iconUrl("plus");
} }
@ -339,10 +333,9 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(
} }
} }
export async function createEmptyNotification() { export async function createEmptyNotification(): Promise<void> {
return new Promise<void>(async (res) => { return new Promise<void>(async (res) => {
if (!swLang.i18n) swLang.fetchLocale(); const i18n = await (swLang.i18n ?? swLang.fetchLocale());
const i18n = (await swLang.i18n) as I18n<any>;
const { t } = i18n; const { t } = i18n;
await self.registration.showNotification( await self.registration.showNotification(

View file

@ -1,7 +1,12 @@
import { get } from "idb-keyval"; import { get } from "idb-keyval";
export async function getAccountFromId(id: string) { export async function getAccountFromId(
const accounts = (await get("accounts")) as { token: string; id: string }[]; id: string,
if (!accounts) console.log("Accounts are not recorded"); ): 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); return accounts.find((e) => e.id === id);
} }

View file

@ -1,6 +1,6 @@
export default function (user: { export function getUserName(user: {
name?: string | null; name?: string | null;
username: string; username: string;
}): string { }): string {
return user.name || user.username; return user.name === "" ? user.username : user.name ?? user.username;
} }

View file

@ -1,4 +1,6 @@
export class I18n<T extends Record<string, any>> { export type Locale = { [key: string]: string | Locale };
export class I18n<T extends Locale = Locale> {
public ts: T; public ts: T;
constructor(locale: T) { constructor(locale: T) {
@ -15,7 +17,8 @@ export class I18n<T extends Record<string, any>> {
try { try {
let str = key let str = key
.split(".") .split(".")
.reduce((o, i) => o[i], this.ts) as unknown as string; .reduce<Locale | Locale[keyof Locale]>((o, i) => o[i], this.ts);
if (typeof str !== "string") throw new Error();
if (args) { if (args) {
for (const [k, v] of Object.entries(args)) { for (const [k, v] of Object.entries(args)) {

View file

@ -1,10 +1,8 @@
/* /*
* Language manager for SW * Language manager for SW
*/ */
declare let self: ServiceWorkerGlobalScope;
import { get, set } from "idb-keyval"; import { get, set } from "idb-keyval";
import { I18n } from "@/scripts/i18n"; import { I18n, type Locale } from "@/scripts/i18n";
class SwLang { class SwLang {
public cacheName = `mk-cache-${_VERSION_}`; public cacheName = `mk-cache-${_VERSION_}`;
@ -14,19 +12,19 @@ class SwLang {
return prelang; return prelang;
}); });
public setLang(newLang: string) { public setLang(newLang: string): Promise<I18n<Locale>> {
this.lang = Promise.resolve(newLang); this.lang = Promise.resolve(newLang);
set("lang", newLang); set("lang", newLang);
return this.fetchLocale(); return this.fetchLocale();
} }
public i18n: Promise<I18n<any>> | null = null; public i18n: Promise<I18n> | null = null;
public fetchLocale() { public fetchLocale(): Promise<I18n<Locale>> {
return this.i18n === this._fetch(); return (this.i18n = this._fetch());
} }
private async _fetch() { private async _fetch(): Promise<I18n<Locale>> {
// Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う // Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う
const localeUrl = `/assets/locales/${await this.lang}.${_VERSION_}.json`; const localeUrl = `/assets/locales/${await this.lang}.${_VERSION_}.json`;
let localeRes = await caches.match(localeUrl); let localeRes = await caches.match(localeUrl);
@ -34,13 +32,13 @@ class SwLang {
// _DEV_がtrueの場合は常に最新化 // _DEV_がtrueの場合は常に最新化
if (!localeRes || _DEV_) { if (!localeRes || _DEV_) {
localeRes = await fetch(localeUrl); localeRes = await fetch(localeUrl);
const clone = localeRes?.clone(); const clone = localeRes.clone();
if (!clone?.clone().ok) Error("locale fetching error"); if (!clone.clone().ok) throw new Error("locale fetching error");
caches.open(this.cacheName).then((cache) => cache.put(localeUrl, clone)); caches.open(this.cacheName).then((cache) => cache.put(localeUrl, clone));
} }
return new I18n(await localeRes.json()); return new I18n<Locale>(await localeRes.json());
} }
} }

View file

@ -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); const u = new URL(url, origin);
u.searchParams.append("loginId", loginId); u.searchParams.set("loginId", loginId);
return u.toString();
}
export function getUrlWithoutLoginId(url: string) {
const u = new URL(url);
u.searchParams.delete("loginId");
return u.toString(); return u.toString();
} }

View file

@ -2,69 +2,100 @@
* Operations * Operations
* *
*/ */
declare let self: ServiceWorkerGlobalScope;
import * as Misskey from "calckey-js"; import * as Misskey from "calckey-js";
import { SwMessage, swMessageOrderType } from "@/types"; import type { SwMessage, SwMessageOrderType } from "@/types";
import { acct as getAcct } from "@/filters/user";
import { getAccountFromId } from "@/scripts/get-account-from-id"; import { getAccountFromId } from "@/scripts/get-account-from-id";
import { getUrlWithLoginId } from "@/scripts/login-id"; import { getUrlWithLoginId } from "@/scripts/login-id";
export const cli = new Misskey.api.APIClient({ export const cli = new Misskey.api.APIClient({
origin, origin,
fetch: (...args) => fetch(...args), fetch: (...args): Promise<Response> => fetch(...args),
}); });
export async function api<E extends keyof Misskey.Endpoints>( export async function api<
E extends keyof Misskey.Endpoints,
O extends Misskey.Endpoints[E]["req"],
>(
endpoint: E, endpoint: E,
userId: string, userId?: string,
options?: Misskey.Endpoints[E]["req"], options?: O,
) { ): Promise<void | ReturnType<typeof cli.request<E, O>>> {
const account = await getAccountFromId(userId); let account: { token: string; id: string } | void;
if (!account) return;
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<string, boolean>();
export function sendMarkAllAsRead(
userId: string,
): Promise<null | undefined | void> {
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からユーザーを開く // rendered acctからユーザーを開く
export function openUser(acct: string, loginId: string) { export function openUser(
acct: string,
loginId?: string,
): ReturnType<typeof openClient> {
return openClient("push", `/@${acct}`, loginId, { acct }); return openClient("push", `/@${acct}`, loginId, { acct });
} }
// noteIdからートを開く // noteIdからートを開く
export function openNote(noteId: string, loginId: string) { export function openNote(
noteId: string,
loginId?: string,
): ReturnType<typeof openClient> {
return openClient("push", `/notes/${noteId}`, loginId, { noteId }); return openClient("push", `/notes/${noteId}`, loginId, { noteId });
} }
export async function openChat(body: any, loginId: string) { // noteIdからートを開く
if (body.groupId === null) { export function openAntenna(
return openClient("push", `/my/messaging/${getAcct(body.user)}`, loginId, { antennaId: string,
body, loginId: string,
): ReturnType<typeof openClient> {
return openClient("push", `/timeline/antenna/${antennaId}`, loginId, {
antennaId,
}); });
} else {
return openClient("push", `/my/messaging/group/${body.groupId}`, loginId, {
body,
});
}
} }
// post-formのオプションから投稿フォームを開く // 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<typeof openClient> {
// クエリを作成しておく // クエリを作成しておく
let url = "/share?"; const url = "/share";
if (options.initialText) url += `text=${options.initialText}&`; const query = new URLSearchParams();
if (options.reply) url += `replyId=${options.reply.id}&`; if (options.initialText) query.set("text", options.initialText);
if (options.renote) url += `renoteId=${options.renote.id}&`; 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( export async function openClient(
order: swMessageOrderType, order: SwMessageOrderType,
url: string, url: string,
loginId: string, loginId?: string,
query: any = {}, query: Record<string, SwMessage[string]> = {},
) { ): Promise<WindowClient | null> {
const client = await findClient(); const client = await findClient();
if (client) { if (client) {
@ -74,19 +105,16 @@ export async function openClient(
order, order,
loginId, loginId,
url, url,
} as SwMessage); } satisfies SwMessage);
return client; return client;
} }
return self.clients.openWindow(getUrlWithLoginId(url, loginId)); return self.clients.openWindow(getUrlWithLoginId(url, loginId!));
} }
export async function findClient() { export async function findClient(): Promise<WindowClient | null> {
const clients = await self.clients.matchAll({ const clients = await globalThis.clients.matchAll({
type: "window", type: "window",
}); });
for (const c of clients) { return clients.find((c) => !new URL(c.url).searchParams.has("zen")) ?? null;
if (c.url.indexOf("?zen") < 0) return c;
}
return null;
} }

View file

@ -1,12 +1,8 @@
export const twemojiSvgBase = "/twemoji";
export function char2fileName(char: string): string { 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(<T>(x: T | undefined): x is T => x !== undefined);
if (!codes.includes("200d")) codes = codes.filter((x) => x !== "fe0f"); 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("-"); return codes.join("-");
} }
export function char2filePath(char: string): string {
return `${twemojiSvgBase}/${char2fileName(char)}.svg`;
}

View file

@ -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<string, any>);
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}`;
}

View file

@ -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 { import {
createEmptyNotification, createEmptyNotification,
createNotification, createNotification,
} from "@/scripts/create-notification"; } from "@/scripts/create-notification";
import { swLang } from "@/scripts/lang"; import { swLang } from "@/scripts/lang";
import { swNotificationRead } from "@/scripts/notification-read";
import { pushNotificationDataMap } from "@/types";
import * as swos from "@/scripts/operations"; import * as swos from "@/scripts/operations";
import { acct as getAcct } from "@/filters/user";
self.addEventListener("install", (ev) => { globalThis.addEventListener("install", () => {
ev.waitUntil(self.skipWaiting()); // ev.waitUntil(globalThis.skipWaiting());
}); });
self.addEventListener("activate", (ev) => { globalThis.addEventListener("activate", (ev) => {
ev.waitUntil( ev.waitUntil(
caches caches
.keys() .keys()
@ -25,11 +23,15 @@ self.addEventListener("activate", (ev) => {
.map((name) => caches.delete(name)), .map((name) => caches.delete(name)),
), ),
) )
.then(() => self.clients.claim()), .then(() => globalThis.clients.claim()),
); );
}); });
self.addEventListener("fetch", (ev) => { function offlineContentHTML(): string {
return `<!doctype html>Offline. Service Worker @${_VERSION_} <button onclick="location.reload()">reload</button>`;
}
globalThis.addEventListener("fetch", (ev) => {
let isHTMLRequest = false; let isHTMLRequest = false;
if (ev.request.headers.get("sec-fetch-dest") === "document") { if (ev.request.headers.get("sec-fetch-dest") === "document") {
isHTMLRequest = true; isHTMLRequest = true;
@ -41,90 +43,68 @@ self.addEventListener("fetch", (ev) => {
if (!isHTMLRequest) return; if (!isHTMLRequest) return;
ev.respondWith( ev.respondWith(
fetch(ev.request).catch( fetch(ev.request).catch(() => {
() => return new Response(offlineContentHTML(), {
new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 }), status: 200,
), headers: {
"content-type": "text/html",
},
});
}),
); );
}); });
self.addEventListener("push", (ev) => { globalThis.addEventListener("push", (ev) => {
// クライアント取得 // クライアント取得
ev.waitUntil( ev.waitUntil(
self.clients globalThis.clients
.matchAll({ .matchAll({
includeUncontrolled: true, includeUncontrolled: true,
type: "window", type: "window",
}) })
.then( .then(async () => {
async <K extends keyof pushNotificationDataMap>( const data: PushNotificationDataMap[keyof PushNotificationDataMap] =
clients: readonly WindowClient[], ev.data?.json();
) => {
const data: pushNotificationDataMap[K] = ev.data?.json();
switch (data.type) { switch (data.type) {
// case 'driveFileCreated': // case 'driveFileCreated':
case "notification": case "notification":
case "unreadMessagingMessage": case "unreadAntennaNote":
// 1日以上経過している場合は無視 // 1日以上経過している場合は無視
if (new Date().getTime() - data.dateTime > 1000 * 60 * 60 * 24) if (new Date().getTime() - data.dateTime > 1000 * 60 * 60 * 24)
break; break;
// クライアントがあったらストリームに接続しているということなので通知しない
if (clients.length !== 0) break;
return createNotification(data); return createNotification(data);
case "readAllNotifications": case "readAllNotifications":
for (const n of await self.registration.getNotifications()) { await globalThis.registration
if (n?.data?.type === "notification") n.close(); .getNotifications()
} .then((notifications) =>
break; notifications.forEach(
case "readAllMessagingMessages": (n) => n.tag !== "read_notification" && n.close(),
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; break;
} }
return createEmptyNotification(); await createEmptyNotification();
}, return;
), }),
); );
}); });
self.addEventListener( (globalThis as unknown as ServiceWorkerGlobalScope).addEventListener(
"notificationclick", "notificationclick",
<K extends keyof pushNotificationDataMap>( (ev: ServiceWorkerGlobalScopeEventMap["notificationclick"]) => {
ev: ServiceWorkerGlobalScopeEventMap["notificationclick"],
) => {
ev.waitUntil( ev.waitUntil(
(async () => { (async (): Promise<void> => {
if (_DEV_) { if (_DEV_) {
console.log("notificationclick", ev.action, ev.notification.data); console.log("notificationclick", ev.action, ev.notification.data);
} }
const { action, notification } = ev; const { action, notification } = ev;
const data: pushNotificationDataMap[K] = notification.data; const data: PushNotificationDataMap[keyof PushNotificationDataMap] =
const { userId: id } = data; notification.data ?? {};
const { userId: loginId } = data;
let client: WindowClient | null = null; let client: WindowClient | null = null;
switch (data.type) { switch (data.type) {
@ -132,57 +112,53 @@ self.addEventListener(
switch (action) { switch (action) {
case "follow": case "follow":
if ("userId" in data.body) if ("userId" in data.body)
await swos.api("following/create", id, { await swos.api("following/create", loginId, {
userId: data.body.userId, userId: data.body.userId,
}); });
break; break;
case "showUser": case "showUser":
if ("user" in data.body) 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 "reply": case "reply":
if ("note" in data.body) if ("note" in data.body)
client = await swos.openPost({ reply: data.body.note }, id); client = await swos.openPost(
{ reply: data.body.note },
loginId,
);
break; break;
case "renote": case "renote":
if ("note" in data.body) if ("note" in data.body)
await swos.api("notes/create", id, { await swos.api("notes/create", loginId, {
renoteId: data.body.note.id, renoteId: data.body.note.id,
}); });
break; break;
case "accept": case "accept":
switch (data.body.type) { switch (data.body.type) {
case "receiveFollowRequest": case "receiveFollowRequest":
await swos.api("following/requests/accept", id, { await swos.api("following/requests/accept", loginId, {
userId: data.body.userId, userId: data.body.userId,
}); });
break; break;
case "groupInvited":
await swos.api("users/groups/invitations/accept", id, {
invitationId: data.body.invitation.id,
});
break;
} }
break; break;
case "reject": case "reject":
switch (data.body.type) { switch (data.body.type) {
case "receiveFollowRequest": case "receiveFollowRequest":
await swos.api("following/requests/reject", id, { await swos.api("following/requests/reject", loginId, {
userId: data.body.userId, userId: data.body.userId,
}); });
break; break;
case "groupInvited":
await swos.api("users/groups/invitations/reject", id, {
invitationId: data.body.invitation.id,
});
break;
} }
break; break;
case "showFollowRequests": case "showFollowRequests":
client = await swos.openClient( client = await swos.openClient(
"push", "push",
"/my/follow-requests", "/my/follow-requests",
id, loginId,
); );
break; break;
default: default:
@ -191,35 +167,61 @@ self.addEventListener(
client = await swos.openClient( client = await swos.openClient(
"push", "push",
"/my/follow-requests", "/my/follow-requests",
id, loginId,
); );
break; break;
case "groupInvited":
client = await swos.openClient("push", "/my/groups", id);
break;
case "reaction": case "reaction":
client = await swos.openNote(data.body.note.id, id); client = await swos.openNote(data.body.note.id, loginId);
break; break;
default: default:
if ("note" in data.body) { 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) { } 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;
} }
} }
break; break;
case "unreadMessagingMessage": case "unreadAntennaNote":
client = await swos.openChat(data.body, id); client = await swos.openAntenna(data.body.antenna.id, loginId);
break; 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) { if (client) {
client.focus(); client.focus();
} }
if (data.type === "notification") { if (data.type === "notification") {
swNotificationRead.then((that) => that.read(data)); await swos.sendMarkAllAsRead(loginId);
} }
notification.close(); notification.close();
@ -228,24 +230,28 @@ self.addEventListener(
}, },
); );
self.addEventListener( (globalThis as unknown as ServiceWorkerGlobalScope).addEventListener(
"notificationclose", "notificationclose",
<K extends keyof pushNotificationDataMap>( (ev: ServiceWorkerGlobalScopeEventMap["notificationclose"]) => {
ev: ServiceWorkerGlobalScopeEventMap["notificationclose"], const data: PushNotificationDataMap[keyof PushNotificationDataMap] =
) => { ev.notification.data;
const data: pushNotificationDataMap[K] = ev.notification.data;
ev.waitUntil(
(async (): Promise<void> => {
if (data.type === "notification") { if (data.type === "notification") {
swNotificationRead.then((that) => that.read(data)); await swos.sendMarkAllAsRead(data.userId);
} }
return;
})(),
);
}, },
); );
self.addEventListener( (globalThis as unknown as ServiceWorkerGlobalScope).addEventListener(
"message", "message",
(ev: ServiceWorkerGlobalScopeEventMap["message"]) => { (ev: ServiceWorkerGlobalScopeEventMap["message"]) => {
ev.waitUntil( ev.waitUntil(
(async () => { (async (): Promise<void> => {
switch (ev.data) { switch (ev.data) {
case "clear": case "clear":
// Cache Storage全削除 // Cache Storage全削除

View file

@ -1,34 +1,51 @@
import * as Misskey from "calckey-js"; import * as Misskey from "calckey-js";
export type swMessageOrderType = "post" | "push"; export type SwMessageOrderType = "post" | "push";
export type SwMessage = { export type SwMessage = {
type: "order"; type: "order";
order: swMessageOrderType; order: SwMessageOrderType;
loginId: string; loginId?: string;
url: string; url: string;
[x: string]: any; [x: string]: unknown;
}; };
// Defined also @/services/push-notification.ts#L7-L14 // Defined also @/services/push-notification.ts#L7-L14
type pushNotificationDataSourceMap = { type PushNotificationDataSourceMap = {
notification: Misskey.entities.Notification; notification: Misskey.entities.Notification;
unreadMessagingMessage: Misskey.entities.MessagingMessage; unreadAntennaNote: {
readNotifications: { notificationIds: string[] }; antenna: { id: string; name: string };
note: Misskey.entities.Note;
};
readAllNotifications: undefined; readAllNotifications: undefined;
readAllMessagingMessages: undefined; readAllMessagingMessages: undefined;
readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string }; readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string };
}; };
export type pushNotificationData< export type PushNotificationData<
K extends keyof pushNotificationDataSourceMap, K extends keyof PushNotificationDataSourceMap,
> = { > = {
type: K; type: K;
body: pushNotificationDataSourceMap[K]; body: PushNotificationDataSourceMap[K];
userId: string; userId: string;
dateTime: number; dateTime: number;
}; };
export type pushNotificationDataMap = { export type PushNotificationDataMap = {
[K in keyof pushNotificationDataSourceMap]: pushNotificationData<K>; [K in keyof PushNotificationDataSourceMap]: PushNotificationData<K>;
}; };
export type BadgeNames =
| "null"
| "antenna"
| "arrow-back-up"
| "at"
| "chart-arrows"
| "circle-check"
| "medal"
| "messages"
| "plus"
| "quote"
| "repeat"
| "user-plus"
| "users";