From 5002effd656abc8c82dbc1bdd904614b8157d8c1 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 --- packages/sw/.eslintrc.js | 24 +++++---- packages/sw/src/@types/global.d.ts | 1 + packages/sw/src/filters/user.ts | 14 ------ .../sw/src/scripts/create-notification.ts | 34 ++++++------- .../sw/src/scripts/get-account-from-id.ts | 9 ++-- packages/sw/src/scripts/get-user-name.ts | 2 +- packages/sw/src/scripts/i18n.ts | 7 ++- packages/sw/src/scripts/lang.ts | 18 +++---- packages/sw/src/scripts/login-id.ts | 10 +--- packages/sw/src/scripts/operations.ts | 49 ++++++++++--------- packages/sw/src/scripts/twemoji-base.ts | 12 ++--- packages/sw/src/scripts/url.ts | 18 ------- packages/sw/src/sw.ts | 22 ++++----- packages/sw/src/types.ts | 15 +++--- 14 files changed, 97 insertions(+), 138 deletions(-) delete mode 100644 packages/sw/src/filters/user.ts delete mode 100644 packages/sw/src/scripts/url.ts diff --git a/packages/sw/.eslintrc.js b/packages/sw/.eslintrc.js index ae9c53244a..b1fd6b5edc 100644 --- a/packages/sw/.eslintrc.js +++ b/packages/sw/.eslintrc.js @@ -1,22 +1,20 @@ module.exports = { root: true, env: { - "node": false + node: false, }, parserOptions: { - "parser": "@typescript-eslint/parser", + parser: '@typescript-eslint/parser', tsconfigRootDir: __dirname, project: ['./tsconfig.json'], }, - extends: [ - "../shared/.eslintrc.js", - ], + extends: ['../shared/.eslintrc.js'], globals: { - "require": false, - "_DEV_": false, - "_LANGS_": false, - "_VERSION_": false, - "_ENV_": false, - "_PERF_PREFIX_": false, - } -} + require: false, + _DEV_: false, + _LANGS_: false, + _VERSION_: false, + _ENV_: false, + _PERF_PREFIX_: false, + }, +}; diff --git a/packages/sw/src/@types/global.d.ts b/packages/sw/src/@types/global.d.ts index 5aaef9412c..a7d176b0b8 100644 --- a/packages/sw/src/@types/global.d.ts +++ b/packages/sw/src/@types/global.d.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any type FIXME = any; declare const _LANGS_: string[][]; diff --git a/packages/sw/src/filters/user.ts b/packages/sw/src/filters/user.ts deleted file mode 100644 index 09437eb19a..0000000000 --- a/packages/sw/src/filters/user.ts +++ /dev/null @@ -1,14 +0,0 @@ -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 index 4d27858253..61ffa7ab0f 100644 --- a/packages/sw/src/scripts/create-notification.ts +++ b/packages/sw/src/scripts/create-notification.ts @@ -1,22 +1,20 @@ /* * Notification manager for SW */ -import { swLang } from '@/scripts/lang'; -import { cli } from '@/scripts/operations'; -import { BadgeNames, 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 closeNotificationsByTags = async (tags: string[]) => { +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) => `/static-assets/tabler-badges/${name}.png`; +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; @@ -25,7 +23,7 @@ const iconUrl = (name: BadgeNames) => `/static-assets/tabler-badges/${name}.png` * 5. Add `badge: iconUrl('icon-name'),` */ -export async function createNotification(data: PushNotificationDataMap[K]) { +export async function createNotification(data: PushNotificationDataMap[K]): Promise { const n = await composeNotification(data); if (n) { @@ -37,8 +35,7 @@ export async function createNotification { - 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) { /* @@ -139,16 +136,16 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif if (reaction.startsWith(':')) { // カスタム絵文字の場合 const name = reaction.substring(1, reaction.length - 1); - badge = `${origin}/emoji/${name}.webp?${url.query({ - badge: '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) { + if (await fetch(badge).then(res => res.status !== 200).catch(() => true)) { badge = iconUrl('plus'); } @@ -226,10 +223,9 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif } } -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 globalThis.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 be4cfaeba4..825829009f 100644 --- a/packages/sw/src/scripts/get-account-from-id.ts +++ b/packages/sw/src/scripts/get-account-from-id.ts @@ -1,7 +1,10 @@ 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 4daf203e06..e7e75ee25f 100644 --- a/packages/sw/src/scripts/get-user-name.ts +++ b/packages/sw/src/scripts/get-user-name.ts @@ -1,3 +1,3 @@ -export default function(user: { name?: string | null, username: string }): string { +export function getUserName(user: { name?: string | null; username: string }): string { 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 3fe88e5514..33e8e4267b 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) { @@ -13,7 +15,8 @@ export class I18n> { // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも public t(key: string, args?: Record): string { try { - let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; + let str = key.split('.').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 39bd333aba..063059908e 100644 --- a/packages/sw/src/scripts/lang.ts +++ b/packages/sw/src/scripts/lang.ts @@ -2,7 +2,7 @@ * Language manager for SW */ 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_}`; @@ -12,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); @@ -32,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 0f9c6be4a9..ce04afd601 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 2fd02f9dcb..428f243ffe 100644 --- a/packages/sw/src/scripts/operations.ts +++ b/packages/sw/src/scripts/operations.ts @@ -3,17 +3,21 @@ * 各種操作 */ import * as Misskey from 'misskey-js'; -import { SwMessage, SwMessageOrderType } from '@/types'; +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) }); +export const cli = new Misskey.api.APIClient({ origin, fetch: (...args): Promise => fetch(...args) }); -export async function api(endpoint: E, userId: string, options?: Misskey.Endpoints[E]['req']) { - const account = await getAccountFromId(userId); - if (!account) return; +export async function api(endpoint: E, 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秒間隔に制限する @@ -24,55 +28,52 @@ export function sendMarkAllAsRead(userId: string): Promise { setTimeout(() => { readBlockingStatus.set(userId, false); - api('notifications/mark-all-as-read', userId) - .then(resolve, resolve); + 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 }); } // noteIdからノートを開く -export function openAntenna(antennaId: string, loginId: string) { +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, url: string, loginId?: string, query: any = {}) { +export async function openClient(order: SwMessageOrderType, url: string, loginId?: string, query: Record = {}): Promise { const client = await findClient(); if (client) { - client.postMessage({ type: 'order', ...query, order, loginId, url } as SwMessage); + client.postMessage({ type: 'order', ...query, order, loginId, url } satisfies SwMessage); return client; } return globalThis.clients.openWindow(loginId ? getUrlWithLoginId(url, loginId) : url); } -export async function findClient() { +export async function findClient(): Promise { const clients = await globalThis.clients.matchAll({ type: 'window', }); - for (const c of clients) { - if (!(new URL(c.url)).searchParams.has('zen')) 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 638aae3284..4b6a5353ea 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 && 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 5255076156..0000000000 --- a/packages/sw/src/scripts/url.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* objを検査して - * 1. 配列に何も入っていない時はクエリを付けない - * 2. プロパティがundefinedの時はクエリを付けない - * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) - */ -export function query(obj: object): 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 c1cde8b3c2..9e0d9f0d1e 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -1,12 +1,12 @@ +import { get } from 'idb-keyval'; +import * as Acct from 'misskey-js/built/acct'; +import type { PushNotificationDataMap } from '@/types'; import { createEmptyNotification, createNotification } from '@/scripts/create-notification'; import { swLang } from '@/scripts/lang'; -import { PushNotificationDataMap } from '@/types'; import * as swos from '@/scripts/operations'; -import { acct as getAcct } from '@/filters/user'; -import { get } from 'idb-keyval'; -globalThis.addEventListener('install', ev => { - //ev.waitUntil(globalThis.skipWaiting()); +globalThis.addEventListener('install', () => { + // ev.waitUntil(globalThis.skipWaiting()); }); globalThis.addEventListener('activate', ev => { @@ -43,7 +43,7 @@ globalThis.addEventListener('push', ev => { ev.waitUntil(globalThis.clients.matchAll({ includeUncontrolled: true, type: 'window', - }).then(async (clients: readonly WindowClient[]) => { + }).then(async () => { const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json(); switch (data.type) { @@ -66,7 +66,7 @@ globalThis.addEventListener('push', ev => { }); globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => { - ev.waitUntil((async () => { + ev.waitUntil((async (): Promise => { if (_DEV_) { console.log('notificationclick', ev.action, ev.notification.data); } @@ -83,7 +83,7 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv if ('userId' in data.body) 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), loginId); + if ('user' in data.body) 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 }, loginId); @@ -120,7 +120,7 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv if ('note' in data.body) { client = await swos.openNote(data.body.note.id, loginId); } else if ('user' in data.body) { - client = await swos.openUser(getAcct(data.body.user), loginId); + client = await swos.openUser(Acct.toString(data.body.user), loginId); } break; } @@ -160,7 +160,7 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => { const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data; - ev.waitUntil((async () => { + ev.waitUntil((async (): Promise => { if (data.type === 'notification') { await swos.sendMarkAllAsRead(data.userId); } @@ -169,7 +169,7 @@ globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEv }); globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => { - ev.waitUntil((async () => { + ev.waitUntil((async (): Promise => { switch (ev.data) { case 'clear': // Cache Storage全削除 diff --git a/packages/sw/src/types.ts b/packages/sw/src/types.ts index 204ec6198d..cbcde7be36 100644 --- a/packages/sw/src/types.ts +++ b/packages/sw/src/types.ts @@ -1,20 +1,20 @@ -import * as Misskey from 'misskey-js'; +import type * as Misskey from 'misskey-js'; export type SwMessageOrderType = 'post' | 'push'; export type SwMessage = { type: 'order'; order: SwMessageOrderType; - loginId: string; + loginId?: string; url: string; - [x: string]: any; + [x: string]: unknown; }; // Defined also @/core/PushNotificationService.ts#L12 type PushNotificationDataSourceMap = { notification: Misskey.entities.Notification; unreadAntennaNote: { - antenna: { id: string, name: string }; + antenna: { id: string; name: string }; note: Misskey.entities.Note; }; readAllNotifications: undefined; @@ -31,8 +31,8 @@ export type PushNotificationDataMap = { [K in keyof PushNotificationDataSourceMap]: PushNotificationData; }; -export type BadgeNames = - 'null' +export type BadgeNames = + | 'null' | 'antenna' | 'arrow-back-up' | 'at' @@ -44,5 +44,4 @@ export type BadgeNames = | 'quote' | 'repeat' | 'user-plus' - | 'users' - ; + | 'users';