From 1d3b67eafb9f4adcb73b4d06c1912f82ce2b3996 Mon Sep 17 00:00:00 2001 From: Lhcfl Date: Mon, 22 Apr 2024 11:01:46 +0800 Subject: [PATCH 01/15] fix types of pizzax --- packages/client/src/pizzax.ts | 19 ++++++++++--------- packages/firefish-js/src/type-utils.ts | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/client/src/pizzax.ts b/packages/client/src/pizzax.ts index 6974df94dc..b5c07714d4 100644 --- a/packages/client/src/pizzax.ts +++ b/packages/client/src/pizzax.ts @@ -5,12 +5,13 @@ import { onUnmounted, ref, watch } from "vue"; import { api } from "./os"; import { useStream } from "./stream"; import { isSignedIn, me } from "@/me"; +import type { TypeUtils } from "firefish-js"; type StateDef = Record< string, { where: "account" | "device" | "deviceAccount"; - default: any; + default: unknown; } >; @@ -82,8 +83,8 @@ export class Storage { for (const [k, v] of Object.entries(state)) { reactiveState[k] = ref(v); } - this.state = state as any; - this.reactiveState = reactiveState as any; + this.state = state as typeof this.state; + this.reactiveState = reactiveState as typeof this.reactiveState; if (isSignedIn) { // なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう) @@ -201,11 +202,11 @@ export class Storage { } } - public push( + public push>( key: K, value: ArrayElement, ): void { - const currentState = this.state[key]; + const currentState = this.state[key] as unknown[]; this.set(key, [...currentState, value]); } @@ -219,10 +220,10 @@ export class Storage { */ public makeGetterSetter( key: K, - getter?: (v: T[K]) => unknown, - setter?: (v: unknown) => T[K], + getter?: (oldV: T[K]["default"]) => T[K]["default"], + setter?: (oldV: T[K]["default"]) => T[K]["default"], ) { - const valueRef = ref(this.state[key]); + const valueRef = ref(this.state[key]) as Ref; const stop = watch(this.reactiveState[key], (val) => { valueRef.value = val; @@ -242,7 +243,7 @@ export class Storage { return valueRef.value; } }, - set: (value: unknown) => { + set: (value: T[K]["default"]) => { const val = setter ? setter(value) : value; this.set(key, val); valueRef.value = val; diff --git a/packages/firefish-js/src/type-utils.ts b/packages/firefish-js/src/type-utils.ts index bf8297487b..bb2d843f45 100644 --- a/packages/firefish-js/src/type-utils.ts +++ b/packages/firefish-js/src/type-utils.ts @@ -1,6 +1,6 @@ import type { Endpoints } from "./api.types"; -type PropertyOfType = { +export type PropertyOfType = { [K in keyof Type]: Type[K] extends U ? K : never; }[keyof Type]; From 93bee484bbbddce5c926e43ced724c96a10d031c Mon Sep 17 00:00:00 2001 From: Lhcfl Date: Tue, 23 Apr 2024 10:44:06 +0800 Subject: [PATCH 02/15] fix types of pizzax.ts --- packages/client/src/pizzax.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/client/src/pizzax.ts b/packages/client/src/pizzax.ts index a33746bf87..0185071f30 100644 --- a/packages/client/src/pizzax.ts +++ b/packages/client/src/pizzax.ts @@ -88,6 +88,7 @@ export class Storage { if (isSignedIn(me)) { // なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう) + // For some reason, if I don't setTimeout, an error occurs in the api function (probably caused by circular references) window.setTimeout(() => { api("i/registry/get-all", { scope: ["client", this.key] }).then( (kvs) => { @@ -105,7 +106,7 @@ export class Storage { } } localStorage.setItem( - `${this.keyForLocalStorage}::cache::${me.id}`, + `${this.keyForLocalStorage}::cache::${me!.id}`, JSON.stringify(cache), ); }, @@ -119,11 +120,12 @@ export class Storage { key, value, }: { - scope: string[]; + scope?: string[]; key: keyof T; value: T[typeof key]["default"]; }) => { if ( + scope == null || scope.length !== 2 || scope[0] !== "client" || scope[1] !== this.key || @@ -136,13 +138,13 @@ export class Storage { const cache = JSON.parse( localStorage.getItem( - `${this.keyForLocalStorage}::cache::${me.id}`, + `${this.keyForLocalStorage}::cache::${me!.id}`, ) || "{}", ); if (cache[key] !== value) { cache[key] = value; localStorage.setItem( - `${this.keyForLocalStorage}::cache::${me.id}`, + `${this.keyForLocalStorage}::cache::${me!.id}`, JSON.stringify(cache), ); } @@ -151,7 +153,7 @@ export class Storage { } } - public set(key: K, value: T[K]["default"]): void { + public set(key: K & string, value: T[K]["default"]): void { if (_DEV_) console.log("set", key, value); this.state[key] = value; @@ -203,14 +205,14 @@ export class Storage { } public push>( - key: K, + key: K & string, value: ArrayElement, ): void { const currentState = this.state[key] as unknown[]; this.set(key, [...currentState, value]); } - public reset(key: keyof T) { + public reset(key: keyof T & string) { this.set(key, this.def[key].default); } @@ -219,7 +221,7 @@ export class Storage { * 主にvue場で設定コントロールのmodelとして使う用 */ public makeGetterSetter( - key: K, + key: K & string, getter?: (oldV: T[K]["default"]) => T[K]["default"], setter?: (oldV: T[K]["default"]) => T[K]["default"], ) { From 6f324a3dcdf02f9832285979d22dcd7fb427a864 Mon Sep 17 00:00:00 2001 From: Lhcfl Date: Tue, 23 Apr 2024 16:56:39 +0800 Subject: [PATCH 03/15] refactor: move ColdDeviceStorage into a module --- packages/client/src/cold-store.ts | 121 +++++++++++++++++++++++++++++ packages/client/src/store.ts | 116 ++------------------------- packages/client/src/stream.ts | 1 + packages/client/src/theme-store.ts | 3 +- packages/firefish-js/src/api.ts | 20 +++-- 5 files changed, 146 insertions(+), 115 deletions(-) create mode 100644 packages/client/src/cold-store.ts diff --git a/packages/client/src/cold-store.ts b/packages/client/src/cold-store.ts new file mode 100644 index 0000000000..63e7782264 --- /dev/null +++ b/packages/client/src/cold-store.ts @@ -0,0 +1,121 @@ +import { ref as vueRef } from "vue"; +import type { UnwrapRef } from "vue"; + +// TODO: 他のタブと永続化されたstateを同期 + +const PREFIX = "miux:"; + +interface Plugin { + id: string; + name: string; + active: boolean; + configData: Record; + token: string; + ast: unknown[]; +} + +import darkTheme from "@/themes/d-rosepine.json5"; +/** + * Storage for configuration information that does not need to be constantly loaded into memory (non-reactive) + */ +import lightTheme from "@/themes/l-rosepinedawn.json5"; + +const ColdStoreDefault = { + lightTheme, + darkTheme, + syncDeviceDarkMode: true, + plugins: [] as Plugin[], + mediaVolume: 0.5, + vibrate: false, + sound_masterVolume: 0.3, + sound_note: { type: "none", volume: 0 }, + sound_noteMy: { type: "syuilo/up", volume: 1 }, + sound_notification: { type: "syuilo/pope2", volume: 1 }, + sound_chat: { type: "syuilo/pope1", volume: 1 }, + sound_chatBg: { type: "syuilo/waon", volume: 1 }, + sound_antenna: { type: "syuilo/triple", volume: 1 }, + sound_channel: { type: "syuilo/square-pico", volume: 1 }, +}; + +const watchers: { + key: string; + callback: (value) => void; +}[] = []; + +function get( + key: T, +): (typeof ColdStoreDefault)[T] { + // TODO: indexedDBにする + // ただしその際はnullチェックではなくキー存在チェックにしないとダメ + // (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある) + const value = localStorage.getItem(PREFIX + key); + if (value == null) { + return ColdStoreDefault[key]; + } else { + return JSON.parse(value); + } +} + +function set( + key: T, + value: (typeof ColdStoreDefault)[T], +): void { + // 呼び出し側のバグ等で undefined が来ることがある + // undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 + if (value === undefined) { + console.error(`attempt to store undefined value for key '${key}'`); + return; + } + + localStorage.setItem(PREFIX + key, JSON.stringify(value)); + + for (const watcher of watchers) { + if (watcher.key === key) watcher.callback(value); + } +} + +function watch( + key: T, + callback: (value: (typeof ColdStoreDefault)[T]) => void, +) { + watchers.push({ key, callback }); +} + +// TODO: VueのcustomRef使うと良い感じになるかも +function ref(key: T) { + const v = get(key); + const r = vueRef(v); + // TODO: このままではwatcherがリークするので開放する方法を考える + watch(key, (v) => { + r.value = v as UnwrapRef; + }); + return r; +} + +/** + * 特定のキーの、簡易的なgetter/setterを作ります + * 主にvue場で設定コントロールのmodelとして使う用 + */ +function makeGetterSetter(key: K) { + // TODO: VueのcustomRef使うと良い感じになるかも + const valueRef = ref(key); + return { + get: () => { + return valueRef.value; + }, + set: (value: (typeof ColdStoreDefault)[K]) => { + const val = value; + set(key, val); + }, + }; +} + +export default { + default: ColdStoreDefault, + watchers, + get, + set, + watch, + ref, + makeGetterSetter, +}; diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index fed9acc035..8443038435 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -1,9 +1,12 @@ -import { markRaw, ref } from "vue"; +import { markRaw } from "vue"; import type { ApiTypes, entities } from "firefish-js"; import { isSignedIn, me } from "./me"; import { Storage } from "./pizzax"; import type { NoteVisibility } from "@/types/note"; +// biome-ignore lint/suspicious/noExplicitAny: +type TODO = any; + export const postFormActions: { title: string; handler: (from, update) => void | Promise; @@ -152,7 +155,7 @@ export const defaultStore = markRaw( type: string; size: "verySmall" | "small" | "medium" | "large" | "veryLarge"; black: boolean; - props: Record; + props: Record; }[], }, widgets: { @@ -161,7 +164,7 @@ export const defaultStore = markRaw( name: string; id: string; place: string | null; - data: Record; + data: Record; }[], }, tl: { @@ -453,109 +456,6 @@ export const defaultStore = markRaw( }), ); -// TODO: 他のタブと永続化されたstateを同期 +import ColdStore from "./cold-store"; -const PREFIX = "miux:"; - -interface Plugin { - id: string; - name: string; - active: boolean; - configData: Record; - token: string; - ast: any[]; -} - -import darkTheme from "@/themes/d-rosepine.json5"; -/** - * Storage for configuration information that does not need to be constantly loaded into memory (non-reactive) - */ -import lightTheme from "@/themes/l-rosepinedawn.json5"; - -export class ColdDeviceStorage { - public static default = { - lightTheme, - darkTheme, - syncDeviceDarkMode: true, - plugins: [] as Plugin[], - mediaVolume: 0.5, - vibrate: false, - sound_masterVolume: 0.3, - sound_note: { type: "none", volume: 0 }, - sound_noteMy: { type: "syuilo/up", volume: 1 }, - sound_notification: { type: "syuilo/pope2", volume: 1 }, - sound_chat: { type: "syuilo/pope1", volume: 1 }, - sound_chatBg: { type: "syuilo/waon", volume: 1 }, - sound_antenna: { type: "syuilo/triple", volume: 1 }, - sound_channel: { type: "syuilo/square-pico", volume: 1 }, - }; - - public static watchers = []; - - public static get( - key: T, - ): (typeof ColdDeviceStorage.default)[T] { - // TODO: indexedDBにする - // ただしその際はnullチェックではなくキー存在チェックにしないとダメ - // (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある) - const value = localStorage.getItem(PREFIX + key); - if (value == null) { - return ColdDeviceStorage.default[key]; - } else { - return JSON.parse(value); - } - } - - public static set( - key: T, - value: (typeof ColdDeviceStorage.default)[T], - ): void { - // 呼び出し側のバグ等で undefined が来ることがある - // undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 - if (value === undefined) { - console.error(`attempt to store undefined value for key '${key}'`); - return; - } - - localStorage.setItem(PREFIX + key, JSON.stringify(value)); - - for (const watcher of this.watchers) { - if (watcher.key === key) watcher.callback(value); - } - } - - public static watch(key, callback) { - this.watchers.push({ key, callback }); - } - - // TODO: VueのcustomRef使うと良い感じになるかも - public static ref(key: T) { - const v = ColdDeviceStorage.get(key); - const r = ref(v); - // TODO: このままではwatcherがリークするので開放する方法を考える - this.watch(key, (v) => { - r.value = v; - }); - return r; - } - - /** - * 特定のキーの、簡易的なgetter/setterを作ります - * 主にvue場で設定コントロールのmodelとして使う用 - */ - public static makeGetterSetter< - K extends keyof typeof ColdDeviceStorage.default, - >(key: K) { - // TODO: VueのcustomRef使うと良い感じになるかも - const valueRef = ColdDeviceStorage.ref(key); - return { - get: () => { - return valueRef.value; - }, - set: (value: unknown) => { - const val = value; - ColdDeviceStorage.set(key, val); - }, - }; - } -} +export const ColdDeviceStorage = ColdStore; diff --git a/packages/client/src/stream.ts b/packages/client/src/stream.ts index 89dda63f08..b620c60c9d 100644 --- a/packages/client/src/stream.ts +++ b/packages/client/src/stream.ts @@ -31,6 +31,7 @@ export function reloadStream() { isReloading = true; stream.close(); + // biome-ignore lint/suspicious/noAssignInExpressions: assign intentionally stream.once("_connected_", () => (isReloading = false)); stream.stream.reconnect(); isReloading = false; diff --git a/packages/client/src/theme-store.ts b/packages/client/src/theme-store.ts index bc344b339d..6f4616878f 100644 --- a/packages/client/src/theme-store.ts +++ b/packages/client/src/theme-store.ts @@ -17,7 +17,8 @@ export async function fetchThemes(): Promise { key: "themes", }); localStorage.setItem(lsCacheKey, JSON.stringify(themes)); - } catch (err) { + // biome-ignore lint/suspicious/noExplicitAny: Safely any + } catch (err: any) { if (err.code === "NO_SUCH_KEY") return; throw err; } diff --git a/packages/firefish-js/src/api.ts b/packages/firefish-js/src/api.ts index 4a7e1f0b64..3cfdfedbfe 100644 --- a/packages/firefish-js/src/api.ts +++ b/packages/firefish-js/src/api.ts @@ -7,10 +7,12 @@ export type APIError = { code: string; message: string; kind: "client" | "server"; - info: Record; + info: Record; }; -export function isAPIError(reason: any): reason is APIError { +// biome-ignore lint/suspicious/noExplicitAny: used it intentially +type ExplicitlyUsedAny = any; +export function isAPIError(reason: ExplicitlyUsedAny): reason is APIError { return reason[MK_API_ERROR] === true; } @@ -24,7 +26,7 @@ export type FetchLike = ( }, ) => Promise<{ status: number; - json(): Promise; + json(): Promise; }>; type IsNeverType = [T] extends [never] ? true : false; @@ -36,7 +38,10 @@ type IsCaseMatched< P extends Endpoints[E]["req"], C extends number, > = IsNeverType< - StrictExtract + StrictExtract< + Endpoints[E]["res"]["$switch"]["$cases"][C], + [P, ExplicitlyUsedAny] + > > extends false ? true : false; @@ -45,7 +50,10 @@ type GetCaseResult< E extends keyof Endpoints, P extends Endpoints[E]["req"], C extends number, -> = StrictExtract[1]; +> = StrictExtract< + Endpoints[E]["res"]["$switch"]["$cases"][C], + [P, ExplicitlyUsedAny] +>[1]; export class APIClient { public origin: string; @@ -70,7 +78,7 @@ export class APIClient { credential?: string | null | undefined, ): Promise< Endpoints[E]["res"] extends { - $switch: { $cases: [any, any][]; $default: any }; + $switch: { $cases: [unknown, unknown][]; $default: unknown }; } ? IsCaseMatched extends true ? GetCaseResult From 62f5c84ca6d57a7aaec0be46e0379af1569912af Mon Sep 17 00:00:00 2001 From: Lhcfl Date: Tue, 23 Apr 2024 21:30:55 +0800 Subject: [PATCH 04/15] fix types --- .../src/server/api/endpoints/release.ts | 2 +- packages/client/src/account.ts | 160 +++++++++--------- .../client/src/components/MkAnnouncement.vue | 6 +- .../src/components/MkManyAnnouncements.vue | 6 +- packages/client/src/components/MkSignin.vue | 2 +- .../client/src/components/MkSigninDialog.vue | 8 +- packages/client/src/components/MkSignup.vue | 6 +- .../client/src/components/MkSignupDialog.vue | 4 +- packages/client/src/components/MkUpdated.vue | 19 ++- packages/client/src/i18n.ts | 1 + packages/client/src/init.ts | 11 +- packages/client/src/nirax.ts | 30 ++-- packages/client/src/os.ts | 3 - .../src/pages/settings/delete-account.vue | 3 +- packages/client/src/plugin.ts | 41 +++-- packages/firefish-js/src/api.types.ts | 48 ++++++ packages/firefish-js/src/entities.ts | 3 +- 17 files changed, 222 insertions(+), 131 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/release.ts b/packages/backend/src/server/api/endpoints/release.ts index f3a2764295..5b85b9a319 100644 --- a/packages/backend/src/server/api/endpoints/release.ts +++ b/packages/backend/src/server/api/endpoints/release.ts @@ -15,7 +15,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async () => { - let release; + let release: unknown; await fetch( "https://firefish.dev/firefish/firefish/-/raw/develop/release.json", diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index cf6fb54915..8419544174 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -7,6 +7,7 @@ import { alert, api, popup, popupMenu, waiting } from "@/os"; import icon from "@/scripts/icon"; import { del, get, set } from "@/scripts/idb-proxy"; import { reloadChannel, unisonReload } from "@/scripts/unison-reload"; +import type { MenuButton, MenuUser } from "./types/menu"; // TODO: 他のタブと永続化されたstateを同期 @@ -16,7 +17,7 @@ export async function signOut() { waiting(); localStorage.removeItem("account"); - await removeAccount(me.id); + await removeAccount(me!.id); const accounts = await getAccounts(); @@ -26,12 +27,9 @@ export async function signOut() { const registration = await navigator.serviceWorker.ready; const push = await registration.pushManager.getSubscription(); if (push) { - await fetch(`${apiUrl}/sw/unregister`, { - method: "POST", - body: JSON.stringify({ - i: me.token, - endpoint: push.endpoint, - }), + await api("sw/unregister", { + endpoint: push.endpoint, + i: me!.token, // FIXME: This parameter seems to be removable but I didn't test it }); } } @@ -117,13 +115,13 @@ function showSuspendedDialog() { export function updateAccount(accountData) { for (const [key, value] of Object.entries(accountData)) { - me[key] = value; + me![key] = value; } localStorage.setItem("account", JSON.stringify(me)); } export async function refreshAccount() { - const accountData = await fetchAccount(me.token); + const accountData = await fetchAccount(me!.token); return updateAccount(accountData); } @@ -186,7 +184,7 @@ export async function openAccountMenu( async function switchAccount(account: entities.UserDetailed) { const storedAccounts = await getAccounts(); - const token = storedAccounts.find((x) => x.id === account.id).token; + const token = storedAccounts.find((x) => x.id === account.id)!.token; switchAccountWithToken(token); } @@ -195,15 +193,15 @@ export async function openAccountMenu( } const storedAccounts = await getAccounts().then((accounts) => - accounts.filter((x) => x.id !== me.id), + accounts.filter((x) => x.id !== me!.id), ); const accountsPromise = api("users/show", { userIds: storedAccounts.map((x) => x.id), }); - function createItem(account: entities.UserDetailed) { + function createItem(account: entities.UserDetailed): MenuUser { return { - type: "user", + type: "user" as const, user: account, active: opts.active != null ? opts.active === account.id : false, action: () => { @@ -218,10 +216,14 @@ export async function openAccountMenu( const accountItemPromises = storedAccounts.map( (a) => - new Promise((res) => { + new Promise((res) => { accountsPromise.then((accounts) => { const account = accounts.find((x) => x.id === a.id); - if (account == null) return res(null); + if (account == null) { + // The user is deleted, remove it + removeAccount(a.id); + return res(null as unknown as MenuUser); + } res(createItem(account)); }); }), @@ -230,74 +232,72 @@ export async function openAccountMenu( if (opts.withExtraOperation) { popupMenu( [ - ...[ - ...(isMobile ?? false - ? [ - { - type: "parent", - icon: `${icon("ph-plus")}`, - text: i18n.ts.addAccount, - children: [ - { - text: i18n.ts.existingAccount, - action: () => { - showSigninDialog(); - }, + ...(isMobile ?? false + ? [ + { + type: "parent" as const, + icon: `${icon("ph-plus")}`, + text: i18n.ts.addAccount, + children: [ + { + text: i18n.ts.existingAccount, + action: () => { + showSigninDialog(); }, - { - text: i18n.ts.createAccount, - action: () => { - createAccount(); - }, + }, + { + text: i18n.ts.createAccount, + action: () => { + createAccount(); }, - ], - }, - ] - : [ - { - type: "link", - text: i18n.ts.profile, - to: `/@${me.username}`, - avatar: me, - }, - null, - ]), - ...(opts.includeCurrentAccount ? [createItem(me)] : []), - ...accountItemPromises, - ...(isMobile ?? false - ? [ - null, - { - type: "link", - text: i18n.ts.profile, - to: `/@${me.username}`, - avatar: me, - }, - ] - : [ - { - type: "parent", - icon: `${icon("ph-plus")}`, - text: i18n.ts.addAccount, - children: [ - { - text: i18n.ts.existingAccount, - action: () => { - showSigninDialog(); - }, + }, + ], + }, + ] + : [ + { + type: "link" as const, + text: i18n.ts.profile, + to: `/@${me!.username}`, + avatar: me!, + }, + null, + ]), + ...(opts.includeCurrentAccount ? [createItem(me!)] : []), + ...accountItemPromises, + ...(isMobile ?? false + ? [ + null, + { + type: "link" as const, + text: i18n.ts.profile, + to: `/@${me!.username}`, + avatar: me!, + }, + ] + : [ + { + type: "parent" as const, + icon: `${icon("ph-plus")}`, + text: i18n.ts.addAccount, + children: [ + { + text: i18n.ts.existingAccount, + action: () => { + showSigninDialog(); }, - { - text: i18n.ts.createAccount, - action: () => { - createAccount(); - }, + }, + { + text: i18n.ts.createAccount, + action: () => { + createAccount(); }, - ], - }, - ]), - ], + }, + ], + }, + ]), ], - ev.currentTarget ?? ev.target, + (ev.currentTarget ?? ev.target) as HTMLElement, { align: "left", }, @@ -305,10 +305,10 @@ export async function openAccountMenu( } else { popupMenu( [ - ...(opts.includeCurrentAccount ? [createItem(me)] : []), + ...(opts.includeCurrentAccount ? [createItem(me!)] : []), ...accountItemPromises, ], - ev.currentTarget ?? ev.target, + (ev.currentTarget ?? ev.target) as HTMLElement, { align: "left", }, diff --git a/packages/client/src/components/MkAnnouncement.vue b/packages/client/src/components/MkAnnouncement.vue index 3af8e0163f..783d511bc8 100644 --- a/packages/client/src/components/MkAnnouncement.vue +++ b/packages/client/src/components/MkAnnouncement.vue @@ -1,5 +1,5 @@