From b6607692885201386e1e9feca6ff577fe65c991e Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 30 Jul 2025 09:30:07 +0900 Subject: perf(frontend): draw-blurhash workerの結果をpostMessageする際にImageBitmapを移譲する (#16330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/workers/draw-blurhash.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/frontend/src') diff --git a/packages/frontend/src/workers/draw-blurhash.ts b/packages/frontend/src/workers/draw-blurhash.ts index 22de6cd3a8..6e49f6bf66 100644 --- a/packages/frontend/src/workers/draw-blurhash.ts +++ b/packages/frontend/src/workers/draw-blurhash.ts @@ -18,5 +18,5 @@ onmessage = (event) => { render(event.data.hash, canvas); const bitmap = canvas.transferToImageBitmap(); - postMessage({ id: event.data.id, bitmap }); + postMessage({ id: event.data.id, bitmap }, [bitmap]); }; -- cgit v1.2.3-freya From 4f653f2fbc9f48f2d3069dd587907ebee667386c Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:30:35 +0900 Subject: enhance(frontend): typed nirax (#16309) * enhance(frontend): typed nirax * migrate router.replace * fix --- packages/frontend/src/components/MkEmojiPicker.vue | 2 +- packages/frontend/src/components/MkPageWindow.vue | 4 +- packages/frontend/src/components/MkSuperMenu.vue | 2 +- packages/frontend/src/components/global/MkA.vue | 4 +- .../src/components/global/StackingRouterView.vue | 2 +- packages/frontend/src/lib/nirax.ts | 138 ++++++++++++++++++++- packages/frontend/src/pages/admin/roles.edit.vue | 12 +- packages/frontend/src/pages/admin/roles.role.vue | 6 +- packages/frontend/src/pages/antenna-timeline.vue | 6 +- packages/frontend/src/pages/channel-editor.vue | 6 +- packages/frontend/src/pages/channel.vue | 6 +- packages/frontend/src/pages/chat/home.home.vue | 12 +- .../frontend/src/pages/chat/home.invitations.vue | 6 +- packages/frontend/src/pages/flash/flash-edit.vue | 6 +- packages/frontend/src/pages/gallery/edit.vue | 12 +- packages/frontend/src/pages/gallery/post.vue | 6 +- packages/frontend/src/pages/lookup.vue | 19 ++- .../frontend/src/pages/page-editor/page-editor.vue | 12 +- packages/frontend/src/pages/page.vue | 6 +- packages/frontend/src/pages/reversi/index.vue | 6 +- packages/frontend/src/pages/search.note.vue | 20 ++- packages/frontend/src/pages/search.user.vue | 20 ++- .../frontend/src/pages/settings/webhook.edit.vue | 2 +- packages/frontend/src/pages/user-list-timeline.vue | 6 +- packages/frontend/src/router.definition.ts | 2 +- packages/frontend/src/router.ts | 2 +- packages/frontend/src/ui/_common_/sw-inject.ts | 2 +- packages/frontend/src/utility/get-user-menu.ts | 13 +- packages/frontend/src/utility/lookup.ts | 20 ++- 29 files changed, 308 insertions(+), 52 deletions(-) (limited to 'packages/frontend/src') diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 68da098439..654aceb8f5 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -495,7 +495,7 @@ function done(query?: string): boolean | void { function settings() { emit('esc'); - router.push('settings/emoji-palette'); + router.push('/settings/emoji-palette'); } onMounted(() => { diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 1310ea6a77..cf60c1ca3e 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -151,7 +151,7 @@ const contextmenu = computed(() => ([{ function back() { history.value.pop(); - windowRouter.replace(history.value.at(-1)!.path); + windowRouter.replaceByPath(history.value.at(-1)!.path); } function reload() { @@ -163,7 +163,7 @@ function close() { } function expand() { - mainRouter.push(windowRouter.getCurrentFullPath(), 'forcePage'); + mainRouter.pushByPath(windowRouter.getCurrentFullPath(), 'forcePage'); windowEl.value?.close(); } diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 3f8d92a61d..5c89a6530d 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -186,7 +186,7 @@ function searchOnKeyDown(ev: KeyboardEvent) { if (ev.key === 'Enter' && searchSelectedIndex.value != null) { ev.preventDefault(); - router.push(searchResult.value[searchSelectedIndex.value].path + '#' + searchResult.value[searchSelectedIndex.value].id); + router.pushByPath(searchResult.value[searchSelectedIndex.value].path + '#' + searchResult.value[searchSelectedIndex.value].id); } else if (ev.key === 'ArrowDown') { ev.preventDefault(); const current = searchSelectedIndex.value ?? -1; diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index 4004db5b12..ae1b4549ec 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -64,7 +64,7 @@ function onContextmenu(ev) { icon: 'ti ti-player-eject', text: i18n.ts.showInPage, action: () => { - router.push(props.to, 'forcePage'); + router.pushByPath(props.to, 'forcePage'); }, }, { type: 'divider' }, { icon: 'ti ti-external-link', @@ -99,6 +99,6 @@ function nav(ev: MouseEvent) { return openWindow(); } - router.push(props.to, ev.ctrlKey ? 'forcePage' : null); + router.pushByPath(props.to, ev.ctrlKey ? 'forcePage' : null); } diff --git a/packages/frontend/src/components/global/StackingRouterView.vue b/packages/frontend/src/components/global/StackingRouterView.vue index c95c74aef3..9e47517244 100644 --- a/packages/frontend/src/components/global/StackingRouterView.vue +++ b/packages/frontend/src/components/global/StackingRouterView.vue @@ -76,7 +76,7 @@ function mount() { function back() { const prev = tabs.value[tabs.value.length - 2]; tabs.value = [...tabs.value.slice(0, tabs.value.length - 1)]; - router.replace(prev.fullPath); + router?.replaceByPath(prev.fullPath); } router.useListener('change', ({ resolved }) => { diff --git a/packages/frontend/src/lib/nirax.ts b/packages/frontend/src/lib/nirax.ts index a166df9eb0..70db47e24e 100644 --- a/packages/frontend/src/lib/nirax.ts +++ b/packages/frontend/src/lib/nirax.ts @@ -58,7 +58,7 @@ export type RouterEvents = { beforeFullPath: string; fullPath: string; route: RouteDef | null; - props: Map | null; + props: Map | null; }) => void; same: () => void; }; @@ -77,6 +77,110 @@ export type PathResolvedResult = { }; }; +//#region Path Types +type Prettify = { + [K in keyof T]: T[K] +} & {}; + +type RemoveNever = { + [K in keyof T as T[K] extends never ? never : K]: T[K]; +} & {}; + +type IsPathParameter = Part extends `${string}:${infer Parameter}` ? Parameter : never; + +type GetPathParamKeys = + Path extends `${infer A}/${infer B}` + ? IsPathParameter | GetPathParamKeys + : IsPathParameter; + +type GetPathParams = Prettify<{ + [Param in GetPathParamKeys as Param extends `${string}?` ? never : Param]: string; +} & { + [Param in GetPathParamKeys as Param extends `${infer OptionalParam}?` ? OptionalParam : never]?: string; +}>; + +type UnwrapReadOnly = T extends ReadonlyArray + ? U + : T extends Readonly + ? U + : T; + +type GetPaths = Def extends { path: infer Path } + ? Path extends string + ? Def extends { children: infer Children } + ? Children extends RouteDef[] + ? Path | `${Path}${FlattenAllPaths}` + : Path + : Path + : never + : never; + +type FlattenAllPaths = GetPaths; + +type GetSinglePathQuery> = RemoveNever< + Def extends { path: infer BasePath, children: infer Children } + ? BasePath extends string + ? Path extends `${BasePath}${infer ChildPath}` + ? Children extends RouteDef[] + ? ChildPath extends FlattenAllPaths + ? GetPathQuery + : Record + : never + : never + : never + : Def['path'] extends Path + ? Def extends { query: infer Query } + ? Query extends Record + ? UnwrapReadOnly<{ [Key in keyof Query]?: string; }> + : Record + : Record + : Record + >; + +type GetPathQuery> = GetSinglePathQuery; + +type RequiredIfNotEmpty> = T extends Record + ? { [Key in K]?: T } + : { [Key in K]: T }; + +type NotRequiredIfEmpty> = T extends Record ? T | undefined : T; + +type GetRouterOperationProps> = NotRequiredIfEmpty> & { + query?: GetPathQuery; + hash?: string; +}>; +//#endregion + +function buildFullPath(args: { + path: string; + params?: Record; + query?: Record; + hash?: string; +}) { + let fullPath = args.path; + + if (args.params) { + for (const key in args.params) { + const value = args.params[key]; + const replaceRegex = new RegExp(`:${key}(\\?)?`, 'g'); + fullPath = fullPath.replace(replaceRegex, value ? encodeURIComponent(value) : ''); + } + } + + if (args.query) { + const queryString = new URLSearchParams(args.query).toString(); + if (queryString) { + fullPath += '?' + queryString; + } + } + + if (args.hash) { + fullPath += '#' + encodeURIComponent(args.hash); + } + + return fullPath; +} + function parsePath(path: string): ParsedPath { const res = [] as ParsedPath; @@ -282,7 +386,7 @@ export class Nirax extends EventEmitter { } } - if (res.route.loginRequired && !this.isLoggedIn) { + if (res.route.loginRequired && !this.isLoggedIn && 'component' in res.route) { res.route.component = this.notFoundPageComponent; res.props.set('showLoginPopup', true); } @@ -310,14 +414,35 @@ export class Nirax extends EventEmitter { return this.currentFullPath; } - public push(fullPath: string, flag?: RouterFlag) { + public push

>(path: P, props?: GetRouterOperationProps, flag?: RouterFlag | null) { + const fullPath = buildFullPath({ + path, + params: props?.params, + query: props?.query, + hash: props?.hash, + }); + this.pushByPath(fullPath, flag); + } + + public replace

>(path: P, props?: GetRouterOperationProps) { + const fullPath = buildFullPath({ + path, + params: props?.params, + query: props?.query, + hash: props?.hash, + }); + this.replaceByPath(fullPath); + } + + /** どうしても必要な場合に使用(パスが確定している場合は `Nirax.push` を使用すること) */ + public pushByPath(fullPath: string, flag?: RouterFlag | null) { const beforeFullPath = this.currentFullPath; if (fullPath === beforeFullPath) { this.emit('same'); return; } if (this.navHook) { - const cancel = this.navHook(fullPath, flag); + const cancel = this.navHook(fullPath, flag ?? undefined); if (cancel) return; } const res = this.navigate(fullPath); @@ -333,14 +458,15 @@ export class Nirax extends EventEmitter { } } - public replace(fullPath: string) { + /** どうしても必要な場合に使用(パスが確定している場合は `Nirax.replace` を使用すること) */ + public replaceByPath(fullPath: string) { const res = this.navigate(fullPath); this.emit('replace', { fullPath: res._parsedRoute.fullPath, }); } - public useListener(event: E, listener: L) { + public useListener(event: E, listener: EventEmitter.EventListener) { this.addListener(event, listener); onBeforeUnmount(() => { diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue index 1a903eedb9..b24b640527 100644 --- a/packages/frontend/src/pages/admin/roles.edit.vue +++ b/packages/frontend/src/pages/admin/roles.edit.vue @@ -72,12 +72,20 @@ async function save() { roleId: role.value.id, ...data.value, }); - router.push('/admin/roles/' + role.value.id); + router.push('/admin/roles/:id', { + params: { + id: role.value.id, + } + }); } else { const created = await os.apiWithDialog('admin/roles/create', { ...data.value, }); - router.push('/admin/roles/' + created.id); + router.push('/admin/roles/:id', { + params: { + id: created.id, + } + }); } } diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index 1816aec21e..c6c3165828 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -88,7 +88,11 @@ const role = reactive(await misskeyApi('admin/roles/show', { })); function edit() { - router.push('/admin/roles/' + role.id + '/edit'); + router.push('/admin/roles/:id/edit', { + params: { + id: role.id, + } + }); } async function del() { diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index 7d2393dba5..88ae39d5e1 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -47,7 +47,11 @@ async function timetravel() { } function settings() { - router.push(`/my/antennas/${props.antennaId}`); + router.push('/my/antennas/:antennaId', { + params: { + antennaId: props.antennaId, + } + }); } function focus() { diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index 72281ea882..80dfb8e84e 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -165,7 +165,11 @@ function save() { os.apiWithDialog('channels/update', params); } else { os.apiWithDialog('channels/create', params).then(created => { - router.push(`/channels/${created.id}`); + router.push('/channels/:channelId', { + params: { + channelId: created.id, + }, + }); }); } } diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 116aabaee2..7ce42ea0cb 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -147,7 +147,11 @@ watch(() => props.channelId, async () => { }, { immediate: true }); function edit() { - router.push(`/channels/${channel.value?.id}/edit`); + router.push('/channels/:channelId/edit', { + params: { + channelId: props.channelId, + } + }); } function openPostForm() { diff --git a/packages/frontend/src/pages/chat/home.home.vue b/packages/frontend/src/pages/chat/home.home.vue index a0853fb0c9..756bf8a342 100644 --- a/packages/frontend/src/pages/chat/home.home.vue +++ b/packages/frontend/src/pages/chat/home.home.vue @@ -86,7 +86,11 @@ function start(ev: MouseEvent) { async function startUser() { // TODO: localOnly は連合に対応したら消す os.selectUser({ localOnly: true }).then(user => { - router.push(`/chat/user/${user.id}`); + router.push('/chat/user/:userId', { + params: { + userId: user.id, + } + }); }); } @@ -101,7 +105,11 @@ async function createRoom() { name: result, }); - router.push(`/chat/room/${room.id}`); + router.push('/chat/room/:roomId', { + params: { + roomId: room.id, + } + }); } async function search() { diff --git a/packages/frontend/src/pages/chat/home.invitations.vue b/packages/frontend/src/pages/chat/home.invitations.vue index 3cbe186e9d..19d57ea205 100644 --- a/packages/frontend/src/pages/chat/home.invitations.vue +++ b/packages/frontend/src/pages/chat/home.invitations.vue @@ -61,7 +61,11 @@ async function join(invitation: Misskey.entities.ChatRoomInvitation) { roomId: invitation.room.id, }); - router.push(`/chat/room/${invitation.room.id}`); + router.push('/chat/room/:roomId', { + params: { + roomId: invitation.room.id, + }, + }); } async function ignore(invitation: Misskey.entities.ChatRoomInvitation) { diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index 4386209f7c..a964b33a52 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -429,7 +429,11 @@ async function save() { script: script.value, visibility: visibility.value, }); - router.push('/play/' + created.id + '/edit'); + router.push('/play/:id/edit', { + params: { + id: created.id, + }, + }); } } diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index 9c0078e15a..cf0d700962 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -85,7 +85,11 @@ async function save() { fileIds: files.value.map(file => file.id), isSensitive: isSensitive.value, }); - router.push(`/gallery/${props.postId}`); + router.push('/gallery/:postId', { + params: { + postId: props.postId, + } + }); } else { const created = await os.apiWithDialog('gallery/posts/create', { title: title.value, @@ -93,7 +97,11 @@ async function save() { fileIds: files.value.map(file => file.id), isSensitive: isSensitive.value, }); - router.push(`/gallery/${created.id}`); + router.push('/gallery/:postId', { + params: { + postId: created.id, + } + }); } } diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index d02b72dd99..eab435c002 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -150,7 +150,11 @@ async function unlike() { } function edit() { - router.push(`/gallery/${post.value.id}/edit`); + router.push('/gallery/:postId/edit', { + params: { + postId: props.postId, + }, + }); } async function reportAbuse() { diff --git a/packages/frontend/src/pages/lookup.vue b/packages/frontend/src/pages/lookup.vue index c969473b19..d5ee0cdf97 100644 --- a/packages/frontend/src/pages/lookup.vue +++ b/packages/frontend/src/pages/lookup.vue @@ -45,11 +45,20 @@ function fetch() { promise = misskeyApi('ap/show', { uri, }); + promise.then(res => { if (res.type === 'User') { - mainRouter.replace(res.object.host ? `/@${res.object.username}@${res.object.host}` : `/@${res.object.username}`); + mainRouter.replace('/@:acct/:page?', { + params: { + acct: res.host != null ? `${res.object.username}@${res.object.host}` : res.object.username, + } + }); } else if (res.type === 'Note') { - mainRouter.replace(`/notes/${res.object.id}`); + mainRouter.replace('/notes/:noteId/:initialTab?', { + params: { + noteId: res.object.id, + } + }); } else { os.alert({ type: 'error', @@ -63,7 +72,11 @@ function fetch() { } promise = misskeyApi('users/show', Misskey.acct.parse(uri)); promise.then(user => { - mainRouter.replace(user.host ? `/@${user.username}@${user.host}` : `/@${user.username}`); + mainRouter.replace('/@:acct/:page?', { + params: { + acct: user.host != null ? `${user.username}@${user.host}` : user.username, + } + }); }); } diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index 8a9b9a9b08..9fe03ae981 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -154,7 +154,11 @@ async function save() { pageId.value = created.id; currentName.value = name.value.trim(); - mainRouter.replace(`/pages/edit/${pageId.value}`); + mainRouter.replace('/pages/edit/:initPageId', { + params: { + initPageId: pageId.value, + }, + }); } } @@ -189,7 +193,11 @@ async function duplicate() { pageId.value = created.id; currentName.value = name.value.trim(); - mainRouter.push(`/pages/edit/${pageId.value}`); + mainRouter.push('/pages/edit/:initPageId', { + params: { + initPageId: pageId.value, + }, + }); } async function add() { diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index cd63e51fd5..5cb13a9c3f 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -267,7 +267,11 @@ function showMenu(ev: MouseEvent) { menuItems.push({ icon: 'ti ti-pencil', text: i18n.ts.edit, - action: () => router.push(`/pages/edit/${page.value.id}`), + action: () => router.push('/pages/edit/:initPageId', { + params: { + initPageId: page.value!.id, + }, + }), }); if ($i.pinnedPageId === page.value.id) { diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index e4d921b8d2..0ae374649d 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -168,7 +168,11 @@ function startGame(game: Misskey.entities.ReversiGameDetailed) { playbackRate: 1, }); - router.push(`/reversi/g/${game.id}`); + router.push('/reversi/g/:gameId', { + params: { + gameId: game.id, + }, + }); } async function matchHeatbeat() { diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index f19c1e7efb..fb34d592a6 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -264,10 +264,18 @@ async function search() { const res = await apLookup(searchParams.value.query); if (res.type === 'User') { - router.push(`/@${res.object.username}@${res.object.host}`); + router.push('/@:acct/:page?', { + params: { + acct: `${res.object.username}@${res.object.host}`, + }, + }); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition } else if (res.type === 'Note') { - router.push(`/notes/${res.object.id}`); + router.push('/notes/:noteId/:initialTab?', { + params: { + noteId: res.object.id, + }, + }); } return; @@ -282,7 +290,7 @@ async function search() { text: i18n.ts.lookupConfirm, }); if (!confirm.canceled) { - router.push(`/${searchParams.value.query}`); + router.pushByPath(`/${searchParams.value.query}`); return; } } @@ -293,7 +301,11 @@ async function search() { text: i18n.ts.openTagPageConfirm, }); if (!confirm.canceled) { - router.push(`/tags/${encodeURIComponent(searchParams.value.query.substring(1))}`); + router.push('/tags/:tag', { + params: { + tag: searchParams.value.query.substring(1), + }, + }); return; } } diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index bd67d41a80..5110fca10c 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -77,10 +77,18 @@ async function search() { const res = await promise; if (res.type === 'User') { - router.push(`/@${res.object.username}@${res.object.host}`); + router.push('/@:acct/:page?', { + params: { + acct: `${res.object.username}@${res.object.host}`, + }, + }); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition } else if (res.type === 'Note') { - router.push(`/notes/${res.object.id}`); + router.push('/notes/:noteId/:initialTab?', { + params: { + noteId: res.object.id, + }, + }); } return; @@ -95,7 +103,7 @@ async function search() { text: i18n.ts.lookupConfirm, }); if (!confirm.canceled) { - router.push(`/${query}`); + router.pushByPath(`/${query}`); return; } } @@ -106,7 +114,11 @@ async function search() { text: i18n.ts.openTagPageConfirm, }); if (!confirm.canceled) { - router.push(`/user-tags/${encodeURIComponent(query.substring(1))}`); + router.push('/user-tags/:tag', { + params: { + tag: query.substring(1), + }, + }); return; } } diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index 877d2deb90..ee387fb20c 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -135,7 +135,7 @@ async function del(): Promise { webhookId: props.webhookId, }); - router.push('/settings/webhook'); + router.push('/settings/connect'); } async function test(type: Misskey.entities.UserWebhook['on'][number]): Promise { diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index f166495258..57a85a0be7 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -42,7 +42,11 @@ watch(() => props.listId, async () => { }, { immediate: true }); function settings() { - router.push(`/my/lists/${props.listId}`); + router.push('/my/lists/:listId', { + params: { + listId: props.listId, + } + }); } const headerActions = computed(() => list.value ? [{ diff --git a/packages/frontend/src/router.definition.ts b/packages/frontend/src/router.definition.ts index 5e0e6f7286..7edc5ed9b7 100644 --- a/packages/frontend/src/router.definition.ts +++ b/packages/frontend/src/router.definition.ts @@ -603,4 +603,4 @@ export const ROUTE_DEF = [{ }, { path: '/:(*)', component: page(() => import('@/pages/not-found.vue')), -}] satisfies RouteDef[]; +}] as const satisfies RouteDef[]; diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 97ca63f50d..b1c1708915 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -20,7 +20,7 @@ export function createRouter(fullPath: string): Router { export const mainRouter = createRouter(window.location.pathname + window.location.search + window.location.hash); window.addEventListener('popstate', (event) => { - mainRouter.replace(window.location.pathname + window.location.search + window.location.hash); + mainRouter.replaceByPath(window.location.pathname + window.location.search + window.location.hash); }); mainRouter.addListener('push', ctx => { diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts index 1459881ba1..63918fbe2f 100644 --- a/packages/frontend/src/ui/_common_/sw-inject.ts +++ b/packages/frontend/src/ui/_common_/sw-inject.ts @@ -43,7 +43,7 @@ export function swInject() { if (mainRouter.currentRoute.value.path === ev.data.url) { return window.scroll({ top: 0, behavior: 'smooth' }); } - return mainRouter.push(ev.data.url); + return mainRouter.pushByPath(ev.data.url); default: return; } diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts index ad0864019b..d4407dadec 100644 --- a/packages/frontend/src/utility/get-user-menu.ts +++ b/packages/frontend/src/utility/get-user-menu.ts @@ -158,7 +158,11 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router icon: 'ti ti-user-exclamation', text: i18n.ts.moderation, action: () => { - router.push(`/admin/user/${user.id}`); + router.push('/admin/user/:userId', { + params: { + userId: user.id, + }, + }); }, }, { type: 'divider' }); } @@ -216,7 +220,12 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router icon: 'ti ti-search', text: i18n.ts.searchThisUsersNotes, action: () => { - router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`); + router.push('/search', { + query: { + username: user.username, + host: user.host ?? undefined, + }, + }); }, }); } diff --git a/packages/frontend/src/utility/lookup.ts b/packages/frontend/src/utility/lookup.ts index 90611094fa..47d0db125d 100644 --- a/packages/frontend/src/utility/lookup.ts +++ b/packages/frontend/src/utility/lookup.ts @@ -19,12 +19,16 @@ export async function lookup(router?: Router) { if (canceled || query.length <= 1) return; if (query.startsWith('@') && !query.includes(' ')) { - _router.push(`/${query}`); + _router.pushByPath(`/${query}`); return; } if (query.startsWith('#')) { - _router.push(`/tags/${encodeURIComponent(query.substring(1))}`); + _router.push('/tags/:tag', { + params: { + tag: query.substring(1), + } + }); return; } @@ -32,9 +36,17 @@ export async function lookup(router?: Router) { const res = await apLookup(query); if (res.type === 'User') { - _router.push(`/@${res.object.username}@${res.object.host}`); + _router.push('/@:acct/:page?', { + params: { + acct: `${res.object.username}@${res.object.host}`, + }, + }); } else if (res.type === 'Note') { - _router.push(`/notes/${res.object.id}`); + _router.push('/notes/:noteId/:initialTab?', { + params: { + noteId: res.object.id, + }, + }); } return; -- cgit v1.2.3-freya From 1dec8b2329c5b82bdd4a55e0ffd9997709feca61 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:12:59 +0900 Subject: fix(frontend/test): Cypressが失敗する問題を修正 (#16307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * attempt to fix test * fix(frontend/test): Cypressが失敗する問題を修正 --- packages/frontend/src/components/MkImgWithBlurhash.vue | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'packages/frontend/src') diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index 361aeff4d0..983a0932c3 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -52,15 +52,20 @@ import TestWebGL2 from '@/workers/test-webgl2?worker'; import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js'; import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js'; +// テスト環境で Web Worker インスタンスは作成できない +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error +const isTest = (import.meta.env.MODE === 'test' || window.Cypress != null); + const canvasPromise = new Promise(resolve => { - // テスト環境で Web Worker インスタンスは作成できない - if (import.meta.env.MODE === 'test') { + if (isTest) { const canvas = window.document.createElement('canvas'); canvas.width = 64; canvas.height = 64; resolve(canvas); return; } + const testWorker = new TestWebGL2(); testWorker.addEventListener('message', event => { if (event.data.result) { @@ -189,7 +194,7 @@ function drawAvg() { } async function draw() { - if (import.meta.env.MODE === 'test' && props.hash == null) return; + if (isTest && props.hash == null) return; drawAvg(); -- cgit v1.2.3-freya From 927aa9dc3d81a4933c6b770e59fa6608970e1c20 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:39:55 +0900 Subject: fix(frontend): inline な SearchMarker のパスが正しくない問題を修正 (#16301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * replace URL path for inlined SearchMarkers The search index looks like: ```ts [ { id: 'foo', label: 'security', path: '/settings/security', inlining: ['2fa'], }, { id: '2fa', label: 'two-factor auth', path: '/settings/2fa', // guessed wrong by the index generation }, { id: 'aaaa', parentId: '2fa', label: 'totp', }, … ] ``` This file post-processes that index and re-parents the inlined sections. Problem was, it left the (wrong) `path` untouched. Replacing the `path` makes the search work fine. * Update Changelog --------- Co-authored-by: dakkar --- CHANGELOG.md | 3 ++- packages/frontend/src/utility/settings-search-index.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'packages/frontend/src') diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f6ca2a862..161a336a8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ - ### Client -- +- Fix: 一部の設定検索結果が存在しないパスになる問題を修正 + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171) ### Server - diff --git a/packages/frontend/src/utility/settings-search-index.ts b/packages/frontend/src/utility/settings-search-index.ts index 7ed97ed34f..8506e4fe2f 100644 --- a/packages/frontend/src/utility/settings-search-index.ts +++ b/packages/frontend/src/utility/settings-search-index.ts @@ -24,6 +24,7 @@ for (const item of generated) { const inline = rootMods.get(id); if (inline) { inline.parentId = item.id; + inline.path = item.path; } else { console.log('[Settings Search Index] Failed to inline', id); } -- cgit v1.2.3-freya From f2a23fb55ef2100bd26e3f2bcd7f939052c2ea09 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:40:51 +0900 Subject: ノートの脱CASCADE削除 (#16332) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Update CHANGELOG.md * Update QueryService.ts * Update QueryService.ts * wip * Update MkNoteDetailed.vue * Update NoteEntityService.ts * wip * Update antennas.ts * Update create.ts * Update NoteEntityService.ts * wip * Update CHANGELOG.md * Update NoteEntityService.ts * Update NoteCreateService.ts * Update note.test.ts * Update note.test.ts * Update ClientServerService.ts * Update ClientServerService.ts * add error handling * Update NoteDeleteService.ts * Update CHANGELOG.md * Update entities.ts * Update entities.ts * Update misskey-js.api.md --- CHANGELOG.md | 6 ++-- locales/index.d.ts | 4 +-- locales/ja-JP.yml | 4 +-- .../1753868431598-remove_note_constraints.js | 18 +++++++++++ packages/backend/src/core/NoteCreateService.ts | 8 +++-- packages/backend/src/core/NoteDeleteService.ts | 36 ---------------------- packages/backend/src/core/QueryService.ts | 4 +-- .../backend/src/core/entities/NoteEntityService.ts | 25 +++++++++++---- packages/backend/src/models/Note.ts | 4 +-- packages/backend/src/server/api/GetterService.ts | 4 +-- .../src/server/api/endpoints/notes/create.ts | 10 ++++-- .../backend/src/server/api/endpoints/notes/show.ts | 2 +- .../backend/src/server/web/ClientServerService.ts | 9 ++++-- packages/backend/test-federation/test/note.test.ts | 2 -- packages/backend/test/e2e/antennas.ts | 1 - packages/frontend/src/components/MkNote.vue | 6 ++-- .../frontend/src/components/MkNoteDetailed.vue | 2 +- packages/frontend/src/components/MkNoteSimple.vue | 18 +++++++++-- packages/frontend/src/components/MkNoteSub.vue | 21 ++++++++++--- packages/misskey-js/etc/misskey-js.api.md | 4 +-- packages/misskey-js/src/entities.ts | 3 +- packages/misskey-js/src/note.ts | 4 +-- 22 files changed, 115 insertions(+), 80 deletions(-) create mode 100644 packages/backend/migration/1753868431598-remove_note_constraints.js (limited to 'packages/frontend/src') diff --git a/CHANGELOG.md b/CHANGELOG.md index 161a336a8b..af5c0da4a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,16 @@ ## Unreleased ### General -- +- ノートを削除した際、関連するノートが同時に削除されないようになりました + - APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります ### Client - Fix: 一部の設定検索結果が存在しないパスになる問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171) ### Server -- +- Enhance: ノートの削除処理の効率化 +- Enhance: 全体的なパフォーマンスの向上 ## 2025.7.0 diff --git a/locales/index.d.ts b/locales/index.d.ts index 8d757ff579..088b89b79f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2567,11 +2567,11 @@ export interface Locale extends ILocale { */ "serviceworkerInfo": string; /** - * 削除された投稿 + * 削除されたノート */ "deletedNote": string; /** - * 非公開の投稿 + * 非公開のノート */ "invisibleNote": string; /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 161edfe8bb..5bd2fc6e17 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -637,8 +637,8 @@ addRelay: "リレーの追加" inboxUrl: "inboxのURL" addedRelays: "追加済みのリレー" serviceworkerInfo: "プッシュ通知を行うには有効にする必要があります。" -deletedNote: "削除された投稿" -invisibleNote: "非公開の投稿" +deletedNote: "削除されたノート" +invisibleNote: "非公開のノート" enableInfiniteScroll: "自動でもっと見る" visibility: "公開範囲" poll: "アンケート" diff --git a/packages/backend/migration/1753868431598-remove_note_constraints.js b/packages/backend/migration/1753868431598-remove_note_constraints.js new file mode 100644 index 0000000000..29540cf9de --- /dev/null +++ b/packages/backend/migration/1753868431598-remove_note_constraints.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RemoveNoteConstraints1753868431598 { + name = 'RemoveNoteConstraints1753868431598' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_52ccc804d7c69037d558bac4c96"`); + await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_52ccc804d7c69037d558bac4c96" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 469426f87e..1eefcfa054 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -421,7 +421,7 @@ export class NoteCreateService implements OnApplicationShutdown { emojis, userId: user.id, localOnly: data.localOnly!, - reactionAcceptance: data.reactionAcceptance, + reactionAcceptance: data.reactionAcceptance ?? null, visibility: data.visibility as any, visibleUserIds: data.visibility === 'specified' ? data.visibleUsers @@ -483,7 +483,11 @@ export class NoteCreateService implements OnApplicationShutdown { await this.notesRepository.insert(insert); } - return insert; + return { + ...insert, + reply: data.reply ?? null, + renote: data.renote ?? null, + }; } catch (e) { // duplicate key error if (isDuplicateKeyValueError(e)) { diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index e394506a44..af1f0eda9a 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -62,7 +62,6 @@ export class NoteDeleteService { */ async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) { const deletedAt = new Date(); - const cascadingNotes = await this.findCascadingNotes(note); if (note.replyId) { await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1); @@ -90,15 +89,6 @@ export class NoteDeleteService { this.deliverToConcerned(user, note, content); } - - // also deliver delete activity to cascaded notes - const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes - for (const cascadingNote of federatedLocalCascadingNotes) { - if (!cascadingNote.user) continue; - if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue; - const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); - this.deliverToConcerned(cascadingNote.user, cascadingNote, content); - } //#endregion this.notesChart.update(note, false); @@ -118,9 +108,6 @@ export class NoteDeleteService { } } - for (const cascadingNote of cascadingNotes) { - this.searchService.unindexNote(cascadingNote); - } this.searchService.unindexNote(note); await this.notesRepository.delete({ @@ -140,29 +127,6 @@ export class NoteDeleteService { } } - @bindThis - private async findCascadingNotes(note: MiNote): Promise { - const recursive = async (noteId: string): Promise => { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.replyId = :noteId', { noteId }) - .orWhere(new Brackets(q => { - q.where('note.renoteId = :noteId', { noteId }) - .andWhere('note.text IS NOT NULL'); - })) - .leftJoinAndSelect('note.user', 'user'); - const replies = await query.getMany(); - - return [ - replies, - ...await Promise.all(replies.map(reply => recursive(reply.id))), - ].flat(); - }; - - const cascadingNotes: MiNote[] = await recursive(note.id); - - return cascadingNotes; - } - @bindThis private async getMentionedRemoteUsers(note: MiNote) { const where = [] as any[]; diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index d398e83230..49f93ad108 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -360,7 +360,7 @@ export class QueryService { public generateSuspendedUserQueryForNote(q: SelectQueryBuilder, excludeAuthor?: boolean): void { if (excludeAuthor) { const brakets = (user: string) => new Brackets(qb => qb - .where(`note.${user}Id IS NULL`) + .where(`${user}.id IS NULL`) // そもそもreplyやrenoteではない、もしくはleftjoinなどでuserが存在しなかった場合を考慮 .orWhere(`user.id = ${user}.id`) .orWhere(`${user}.isSuspended = FALSE`)); q @@ -368,7 +368,7 @@ export class QueryService { .andWhere(brakets('renoteUser')); } else { const brakets = (user: string) => new Brackets(qb => qb - .where(`note.${user}Id IS NULL`) + .where(`${user}.id IS NULL`) // そもそもreplyやrenoteではない、もしくはleftjoinなどでuserが存在しなかった場合を考慮 .orWhere(`${user}.isSuspended = FALSE`)); q .andWhere('user.isSuspended = FALSE') diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 92caad908c..6871ba2c72 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; +import { EntityNotFoundError, In } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { Packed } from '@/misc/json-schema.js'; @@ -46,6 +46,17 @@ function getAppearNoteIds(notes: MiNote[]): Set { return appearNoteIds; } +async function nullIfEntityNotFound(promise: Promise): Promise { + try { + return await promise; + } catch (err) { + if (err instanceof EntityNotFoundError) { + return null; + } + throw err; + } +} + @Injectable() export class NoteEntityService implements OnModuleInit { private userEntityService: UserEntityService; @@ -436,19 +447,21 @@ export class NoteEntityService implements OnModuleInit { ...(opts.detail ? { clippedCount: note.clippedCount, - reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { + // そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される + reply: (note.replyId && note.reply === null) ? null : note.replyId ? nullIfEntityNotFound(this.pack(note.reply ?? note.replyId, me, { detail: false, skipHide: opts.skipHide, withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, - }) : undefined, + })) : undefined, - renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, { + // そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される + renote: (note.renoteId && note.renote === null) ? null : note.renoteId ? nullIfEntityNotFound(this.pack(note.renote ?? note.renoteId, me, { detail: true, skipHide: opts.skipHide, withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, - }) : undefined, + })) : undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, @@ -591,7 +604,7 @@ export class NoteEntityService implements OnModuleInit { private findNoteOrFail(id: string): Promise { return this.notesRepository.findOneOrFail({ where: { id }, - relations: ['user'], + relations: ['user', 'renote', 'reply'], }); } diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index 9822ec94e4..ff46615729 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -36,7 +36,7 @@ export class MiNote { public replyId: MiNote['id'] | null; @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', + createForeignKeyConstraints: false, }) @JoinColumn() public reply: MiNote | null; @@ -50,7 +50,7 @@ export class MiNote { public renoteId: MiNote['id'] | null; @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', + createForeignKeyConstraints: false, }) @JoinColumn() public renote: MiNote | null; diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index 444e6db744..8f4213dfb6 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -40,8 +40,8 @@ export class GetterService { } @bindThis - public async getNoteWithUser(noteId: MiNote['id']) { - const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] }); + public async getNoteWithRelations(noteId: MiNote['id']) { + const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] }); if (note == null) { throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 253a360815..7caea8eedc 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -269,7 +269,10 @@ export default class extends Endpoint { // eslint- let renote: MiNote | null = null; if (ps.renoteId != null) { // Fetch renote to note - renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); + renote = await this.notesRepository.findOne({ + where: { id: ps.renoteId }, + relations: ['user', 'renote', 'reply'], + }); if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); @@ -315,7 +318,10 @@ export default class extends Endpoint { // eslint- let reply: MiNote | null = null; if (ps.replyId != null) { // Fetch reply - reply = await this.notesRepository.findOneBy({ id: ps.replyId }); + reply = await this.notesRepository.findOne({ + where: { id: ps.replyId }, + relations: ['user'], + }); if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index b93c73b0c5..cae0e752da 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -55,7 +55,7 @@ export default class extends Endpoint { // eslint- private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { - const note = await this.getterService.getNoteWithUser(ps.noteId).catch(err => { + const note = await this.getterService.getNoteWithRelations(ps.noteId).catch(err => { if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; }); diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 8ca61a497d..4d122b0fcf 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -580,7 +580,7 @@ export class ClientServerService { id: request.params.note, visibility: In(['public', 'home']), }, - relations: ['user'], + relations: ['user', 'reply', 'renote'], }); if ( @@ -821,8 +821,11 @@ export class ClientServerService { fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => { reply.removeHeader('X-Frame-Options'); - const note = await this.notesRepository.findOneBy({ - id: request.params.note, + const note = await this.notesRepository.findOne({ + where: { + id: request.params.note, + }, + relations: ['user', 'reply', 'renote'], }); if (note == null) return; diff --git a/packages/backend/test-federation/test/note.test.ts b/packages/backend/test-federation/test/note.test.ts index 1584f9587e..a339cd86d2 100644 --- a/packages/backend/test-federation/test/note.test.ts +++ b/packages/backend/test-federation/test/note.test.ts @@ -63,7 +63,6 @@ describe('Note', () => { deepStrictEqualWithExcludedFields(note, resolvedNote, [ 'id', 'emojis', - 'reactionAcceptance', 'replyId', 'reply', 'userId', @@ -105,7 +104,6 @@ describe('Note', () => { deepStrictEqualWithExcludedFields(note, resolvedNote, [ 'id', 'emojis', - 'reactionAcceptance', 'renoteId', 'renote', 'userId', diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index 4dbeacf925..1bbacd065b 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -673,7 +673,6 @@ describe('アンテナ', () => { assert.deepStrictEqual(response, expected); }); - test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { }); test.each([ { label: 'ID指定', offsetBy: 'id' }, diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 0605030d5b..b9cb37e99a 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]" tabindex="0" > - +

{{ i18n.ts.pinnedNote }}
@@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -282,7 +282,7 @@ let note = deepClone(props.note); //} const isRenote = Misskey.note.isPureRenote(note); -const appearNote = getAppearNote(note); +const appearNote = getAppearNote(note) ?? note; const { $note: $appearNote, subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({ note: appearNote, parentNote: note, diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index fb37bb1ae6..c04959b97a 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index e684cf2a30..f1107527b7 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> + + diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index f6a2eb1c27..d079b4cb0c 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -287,6 +287,10 @@ SPDX-License-Identifier: AGPL-3.0-only
+ + + Open setup wizard + @@ -425,6 +429,20 @@ const proxyAccountForm = useForm({ fetchInstance(true); }); +async function openSetupWizard() { + const { canceled } = await os.confirm({ + type: 'warning', + title: i18n.ts._serverSettings.restartServerSetupWizardConfirm_title, + text: i18n.ts._serverSettings.restartServerSetupWizardConfirm_text, + }); + if (canceled) return; + + const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkServerSetupWizardDialog.vue').then(x => x.default), { + }, { + closed: () => dispose(), + }); +} + const headerTabs = computed(() => []); definePage(() => ({ diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index 3e2d086858..393ba98d30 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -87,7 +87,14 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._serverSetupWizard.settingsYouMakeHereCanBeChangedLater }}
- + + + + {{ i18n.ts._serverSetupWizard.skipSettings }} -- cgit v1.2.3-freya From d624da9c1aac731bd49a7bbb949744ebf4986479 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 1 Aug 2025 11:49:12 +0900 Subject: feat: remote notes cleaning (#16292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * wip * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update job-queue.job.vue * wip * Update CleanRemoteNotesProcessorService.ts * wip * wip * wip * Update CleanRemoteNotesProcessorService.ts * wip * Update CHANGELOG.md * Revert "wip" This reverts commit 89d455d302c1106c421bcec309fd7bf02509465e. * wip * woip * Update QueueService.ts * Update QueueService.ts * ピン留め考慮 * Update CleanRemoteNotesProcessorService.ts * Update QueueService.ts * Update CleanRemoteNotesProcessorService.ts * add log * Update CHANGELOG.md * wip * Update MkServerSetupWizard.vue --- CHANGELOG.md | 3 + locales/index.d.ts | 32 ++++ locales/ja-JP.yml | 8 + .../migration/1753863104203-remoteNotesCleaning.js | 20 +++ packages/backend/src/core/QueueService.ts | 4 + packages/backend/src/models/Meta.ts | 15 ++ packages/backend/src/queue/QueueProcessorModule.ts | 4 +- .../backend/src/queue/QueueProcessorService.ts | 3 + .../processors/CleanRemoteNotesProcessorService.ts | 174 +++++++++++++++++++++ .../backend/src/server/api/endpoints/admin/meta.ts | 15 ++ .../src/server/api/endpoints/admin/update-meta.ts | 15 ++ .../src/components/MkServerSetupWizard.vue | 15 +- .../frontend/src/pages/admin/job-queue.job.vue | 4 +- packages/frontend/src/pages/admin/performance.vue | 42 +++++ packages/misskey-js/src/autogen/types.ts | 6 + 15 files changed, 356 insertions(+), 4 deletions(-) create mode 100644 packages/backend/migration/1753863104203-remoteNotesCleaning.js create mode 100644 packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts (limited to 'packages/frontend/src') diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e2a82e574..4025f8ab44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ ### General - ノートを削除した際、関連するノートが同時に削除されないようになりました - APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります +- 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning) + - **デフォルトでオン**になっています + - データベースの肥大化を防止することが可能です ### Client - Fix: 一部の設定検索結果が存在しないパスになる問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index 1be8811a25..6bb6d59476 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5493,6 +5493,14 @@ export interface Locale extends ILocale { * 低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。 */ "defaultImageCompressionLevel_description": string; + /** + * 分 + */ + "inMinutes": string; + /** + * 日 + */ + "inDays": string; "_order": { /** * 新しい順 @@ -6486,6 +6494,22 @@ export interface Locale extends ILocale { * 有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。 */ "reactionsBufferingDescription": string; + /** + * リモート投稿の自動クリーニング + */ + "remoteNotesCleaning": string; + /** + * 有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。 + */ + "remoteNotesCleaning_description": string; + /** + * 最大クリーニング処理継続時間 + */ + "remoteNotesCleaningMaxProcessingDuration": string; + /** + * 最低ノート保持日数 + */ + "remoteNotesCleaningExpiryDaysForEachNotes": string; /** * 問い合わせ先URL */ @@ -11951,6 +11975,14 @@ export interface Locale extends ILocale { * 連合可能なサーバーの指定など、高度な設定も後ほど可能です。 */ "youCanConfigureMoreFederationSettingsLater": string; + /** + * 受信コンテンツの自動クリーニング + */ + "remoteContentsCleaning": string; + /** + * 連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。 + */ + "remoteContentsCleaning_description": string; /** * 管理者情報 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d4edfc5aab..f141d23ecc 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1368,6 +1368,8 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示" hideAllTips: "全ての「ヒントとコツ」を非表示" defaultImageCompressionLevel: "デフォルトの画像圧縮度" defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。" +inMinutes: "分" +inDays: "日" _order: newest: "新しい順" @@ -1649,6 +1651,10 @@ _serverSettings: fanoutTimelineDbFallback: "データベースへのフォールバック" fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。" reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。" + remoteNotesCleaning: "リモート投稿の自動クリーニング" + remoteNotesCleaning_description: "有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。" + remoteNotesCleaningMaxProcessingDuration: "最大クリーニング処理継続時間" + remoteNotesCleaningExpiryDaysForEachNotes: "最低ノート保持日数" inquiryUrl: "問い合わせ先URL" inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。" openRegistration: "アカウントの作成をオープンにする" @@ -3196,6 +3202,8 @@ _serverSetupWizard: doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。" doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。" youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。" + remoteContentsCleaning: "受信コンテンツの自動クリーニング" + remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。" adminInfo: "管理者情報" adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。" adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。" diff --git a/packages/backend/migration/1753863104203-remoteNotesCleaning.js b/packages/backend/migration/1753863104203-remoteNotesCleaning.js new file mode 100644 index 0000000000..37d42a571d --- /dev/null +++ b/packages/backend/migration/1753863104203-remoteNotesCleaning.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RemoteNotesCleaning1753863104203 { + name = 'RemoteNotesCleaning1753863104203' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableRemoteNotesCleaning" boolean NOT NULL DEFAULT true`); + await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningMaxProcessingDurationInMinutes" integer NOT NULL DEFAULT \'60\''); + await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningExpiryDaysForEachNotes" integer NOT NULL DEFAULT \'90\''); + } + + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "meta" DROP COLUMN "remoteNotesCleaningExpiryDaysForEachNotes"'); + await queryRunner.query('ALTER TABLE "meta" DROP COLUMN "remoteNotesCleaningMaxProcessingDurationInMinutes"'); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableRemoteNotesCleaning"`); + } +} diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 2e49f8cf5e..06170b242a 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -78,6 +78,10 @@ const REPEATABLE_SYSTEM_JOB_DEF = [{ name: 'checkModeratorsActivity', // 毎時30分に起動 pattern: '30 * * * *', +}, { + name: 'cleanRemoteNotes', + // 毎日午前4時に起動(最も人の少ない時間帯) + pattern: '0 4 * * *', }]; @Injectable() diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 85c10ab666..c97fcd8dfc 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -701,6 +701,21 @@ export class MiMeta { default: true, }) public allowExternalApRedirect: boolean; + + @Column('boolean', { + default: true, + }) + public enableRemoteNotesCleaning: boolean; + + @Column('integer', { + default: 60, // minutes + }) + public remoteNotesCleaningMaxProcessingDurationInMinutes: number; + + @Column('integer', { + default: 90, // days + }) + public remoteNotesCleaningExpiryDaysForEachNotes: number; } export type SoftwareSuspension = { diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 9044285bf6..e01414cd53 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -6,7 +6,6 @@ import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; import { GlobalModule } from '@/GlobalModule.js'; -import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueProcessorService } from './QueueProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; @@ -18,6 +17,8 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; +import { CheckModeratorsActivityProcessorService } from './processors/CheckModeratorsActivityProcessorService.js'; +import { CleanRemoteNotesProcessorService } from './processors/CleanRemoteNotesProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; @@ -83,6 +84,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor AggregateRetentionProcessorService, CheckExpiredMutingsProcessorService, CheckModeratorsActivityProcessorService, + CleanRemoteNotesProcessorService, QueueProcessorService, ], exports: [ diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index c98ebcdcd9..7b64182754 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -43,6 +43,7 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; +import { CleanRemoteNotesProcessorService } from './processors/CleanRemoteNotesProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QUEUE, baseWorkerOptions } from './const.js'; @@ -123,6 +124,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService, private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService, private cleanProcessorService: CleanProcessorService, + private cleanRemoteNotesProcessorService: CleanRemoteNotesProcessorService, ) { this.logger = this.queueLoggerService.logger; @@ -164,6 +166,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process(); case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process(); case 'clean': return this.cleanProcessorService.process(); + case 'cleanRemoteNotes': return this.cleanRemoteNotesProcessorService.process(job); default: throw new Error(`unrecognized job type ${job.name} for system`); } }; diff --git a/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts new file mode 100644 index 0000000000..5b682e20b8 --- /dev/null +++ b/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts @@ -0,0 +1,174 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { setTimeout } from 'node:timers/promises'; +import { Inject, Injectable } from '@nestjs/common'; +import { And, In, IsNull, LessThan, MoreThan, Not } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { MiMeta, MiNote, NoteFavoritesRepository, NotesRepository, UserNotePiningsRepository } from '@/models/_.js'; +import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; + +@Injectable() +export class CleanRemoteNotesProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.meta) + private meta: MiMeta, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.noteFavoritesRepository) + private noteFavoritesRepository: NoteFavoritesRepository, + + @Inject(DI.userNotePiningsRepository) + private userNotePiningsRepository: UserNotePiningsRepository, + + private idService: IdService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('clean-remote-notes'); + } + + @bindThis + public async process(job: Bull.Job>): Promise<{ + deletedCount: number; + oldest: number | null; + newest: number | null; + skipped?: boolean; + }> { + if (!this.meta.enableRemoteNotesCleaning) { + this.logger.info('Remote notes cleaning is disabled, skipping...'); + return { + deletedCount: 0, + oldest: null, + newest: null, + skipped: true, + }; + } + + this.logger.info('cleaning remote notes...'); + + const maxDuration = this.meta.remoteNotesCleaningMaxProcessingDurationInMinutes * 60 * 1000; // Convert minutes to milliseconds + const startAt = Date.now(); + + const MAX_NOTE_COUNT_PER_QUERY = 50; + + const stats = { + deletedCount: 0, + oldest: null as number | null, + newest: null as number | null, + }; + + let cursor: MiNote['id'] = this.idService.gen(Date.now() - (1000 * 60 * 60 * 24 * this.meta.remoteNotesCleaningExpiryDaysForEachNotes)); + + while (true) { + const batchBeginAt = Date.now(); + + let notes: Pick[] = await this.notesRepository.find({ + where: { + id: LessThan(cursor), + userHost: Not(IsNull()), + clippedCount: 0, + renoteCount: 0, + }, + take: MAX_NOTE_COUNT_PER_QUERY, + order: { + // 新しい順 + // https://github.com/misskey-dev/misskey/pull/16292#issuecomment-3139376314 + id: -1, + }, + select: ['id'], + }); + + const fetchedCount = notes.length; + + for (const note of notes) { + if (note.id < cursor) { + cursor = note.id; + } + } + + const pinings = notes.length === 0 ? [] : await this.userNotePiningsRepository.find({ + where: { + noteId: In(notes.map(note => note.id)), + }, + select: ['noteId'], + }); + + notes = notes.filter(note => { + return !pinings.some(pining => pining.noteId === note.id); + }); + + const favorites = notes.length === 0 ? [] : await this.noteFavoritesRepository.find({ + where: { + noteId: In(notes.map(note => note.id)), + }, + select: ['noteId'], + }); + + notes = notes.filter(note => { + return !favorites.some(favorite => favorite.noteId === note.id); + }); + + const replies = notes.length === 0 ? [] : await this.notesRepository.find({ + where: { + replyId: In(notes.map(note => note.id)), + userHost: IsNull(), + }, + select: ['replyId'], + }); + + notes = notes.filter(note => { + return !replies.some(reply => reply.replyId === note.id); + }); + + if (notes.length > 0) { + await this.notesRepository.delete(notes.map(note => note.id)); + + for (const note of notes) { + const t = this.idService.parse(note.id).date.getTime(); + if (stats.oldest === null || t < stats.oldest) { + stats.oldest = t; + } + if (stats.newest === null || t > stats.newest) { + stats.newest = t; + } + } + + stats.deletedCount += notes.length; + } + + job.log(`Deleted ${notes.length} of ${fetchedCount}; ${Date.now() - batchBeginAt}ms`); + + const elapsed = Date.now() - startAt; + + if (elapsed >= maxDuration) { + this.logger.info(`Reached maximum duration of ${maxDuration}ms, stopping...`); + job.log('Reached maximum duration, stopping cleaning.'); + job.updateProgress(100); + break; + } + + job.updateProgress((elapsed / maxDuration) * 100); + + await setTimeout(1000 * 5); // Wait a moment to avoid overwhelming the db + } + + this.logger.succ('cleaning of remote notes completed.'); + + return { + deletedCount: stats.deletedCount, + oldest: stats.oldest, + newest: stats.newest, + skipped: false, + }; + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 924163afbb..4d3f6d6cd8 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -571,6 +571,18 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + enableRemoteNotesCleaning: { + type: 'boolean', + optional: false, nullable: false, + }, + remoteNotesCleaningExpiryDaysForEachNotes: { + type: 'number', + optional: false, nullable: false, + }, + remoteNotesCleaningMaxProcessingDurationInMinutes: { + type: 'number', + optional: false, nullable: false, + }, }, }, } as const; @@ -722,6 +734,9 @@ export default class extends Endpoint { // eslint- proxyRemoteFiles: instance.proxyRemoteFiles, signToActivityPubGet: instance.signToActivityPubGet, allowExternalApRedirect: instance.allowExternalApRedirect, + enableRemoteNotesCleaning: instance.enableRemoteNotesCleaning, + remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes, + remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 578aa2b662..08cea23119 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -205,6 +205,9 @@ export const paramDef = { proxyRemoteFiles: { type: 'boolean' }, signToActivityPubGet: { type: 'boolean' }, allowExternalApRedirect: { type: 'boolean' }, + enableRemoteNotesCleaning: { type: 'boolean' }, + remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' }, + remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' }, }, required: [], } as const; @@ -723,6 +726,18 @@ export default class extends Endpoint { // eslint- set.allowExternalApRedirect = ps.allowExternalApRedirect; } + if (ps.enableRemoteNotesCleaning !== undefined) { + set.enableRemoteNotesCleaning = ps.enableRemoteNotesCleaning; + } + + if (ps.remoteNotesCleaningExpiryDaysForEachNotes !== undefined) { + set.remoteNotesCleaningExpiryDaysForEachNotes = ps.remoteNotesCleaningExpiryDaysForEachNotes; + } + + if (ps.remoteNotesCleaningMaxProcessingDurationInMinutes !== undefined) { + set.remoteNotesCleaningMaxProcessingDurationInMinutes = ps.remoteNotesCleaningMaxProcessingDurationInMinutes; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/frontend/src/components/MkServerSetupWizard.vue b/packages/frontend/src/components/MkServerSetupWizard.vue index e5614d63d7..23e0e85bc9 100644 --- a/packages/frontend/src/components/MkServerSetupWizard.vue +++ b/packages/frontend/src/components/MkServerSetupWizard.vue @@ -55,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}
{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}
+
{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}
{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}
{{ i18n.ts.learnMore }}
@@ -63,6 +63,11 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }} + + + + +
@@ -110,6 +115,10 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.federation }}:
{{ serverSettings.federation === 'none' ? i18n.ts.no : i18n.ts.all }}
+
+
{{ i18n.ts._serverSettings.remoteNotesCleaning }}:
+
{{ serverSettings.enableRemoteNotesCleaning ? i18n.ts.yes : i18n.ts.no }}
+
FTT:
{{ serverSettings.enableFanoutTimeline ? i18n.ts.yes : i18n.ts.no }}
@@ -185,7 +194,9 @@ import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import MkFolder from '@/components/MkFolder.vue'; import MkRadios from '@/components/MkRadios.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; import MkInfo from '@/components/MkInfo.vue'; +import MkLink from '@/components/MkLink.vue'; const emit = defineEmits<{ (ev: 'finished'): void; @@ -202,6 +213,7 @@ const q_name = ref(currentMeta.name ?? ''); const q_use = ref('single'); const q_scale = ref('small'); const q_federation = ref(currentMeta.federation === 'none' ? 'no' : 'yes'); +const q_remoteContentsCleaning = ref(currentMeta.enableRemoteNotesCleaning); const q_adminName = ref(currentMeta.maintainerName ?? ''); const q_adminEmail = ref(currentMeta.maintainerEmail ?? ''); @@ -219,6 +231,7 @@ const serverSettings = computed(() => { emailRequiredForSignup: q_use.value === 'open', enableIpLogging: q_use.value === 'open', federation: q_federation.value === 'yes' ? 'all' : 'none', + enableRemoteNotesCleaning: q_remoteContentsCleaning.value, enableFanoutTimeline: true, enableFanoutTimelineDbFallback: q_use.value === 'single', enableReactionsBuffering, diff --git a/packages/frontend/src/pages/admin/job-queue.job.vue b/packages/frontend/src/pages/admin/job-queue.job.vue index 659aa02b50..4ecdb74199 100644 --- a/packages/frontend/src/pages/admin/job-queue.job.vue +++ b/packages/frontend/src/pages/admin/job-queue.job.vue @@ -98,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -150,7 +150,7 @@ SPDX-License-Identifier: AGPL-3.0-only Update
- +
diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue index c28621b11e..ff3a5b9d7f 100644 --- a/packages/frontend/src/pages/admin/performance.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -101,6 +101,35 @@ SPDX-License-Identifier: AGPL-3.0-only
+ + + + + + + + +
+ + + + + + +
+
@@ -196,6 +225,19 @@ const rbtForm = useForm({ fetchInstance(true); }); +const remoteNotesCleaningForm = useForm({ + enableRemoteNotesCleaning: meta.enableRemoteNotesCleaning, + remoteNotesCleaningExpiryDaysForEachNotes: meta.remoteNotesCleaningExpiryDaysForEachNotes, + remoteNotesCleaningMaxProcessingDurationInMinutes: meta.remoteNotesCleaningMaxProcessingDurationInMinutes, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableRemoteNotesCleaning: state.enableRemoteNotesCleaning, + remoteNotesCleaningExpiryDaysForEachNotes: state.remoteNotesCleaningExpiryDaysForEachNotes, + remoteNotesCleaningMaxProcessingDurationInMinutes: state.remoteNotesCleaningMaxProcessingDurationInMinutes, + }); + fetchInstance(true); +}); + const headerActions = computed(() => []); const headerTabs = computed(() => []); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 7594117deb..c0a6dca67e 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -9370,6 +9370,9 @@ export interface operations { proxyRemoteFiles: boolean; signToActivityPubGet: boolean; allowExternalApRedirect: boolean; + enableRemoteNotesCleaning: boolean; + remoteNotesCleaningExpiryDaysForEachNotes: number; + remoteNotesCleaningMaxProcessingDurationInMinutes: number; }; }; }; @@ -12599,6 +12602,9 @@ export interface operations { proxyRemoteFiles?: boolean; signToActivityPubGet?: boolean; allowExternalApRedirect?: boolean; + enableRemoteNotesCleaning?: boolean; + remoteNotesCleaningExpiryDaysForEachNotes?: number; + remoteNotesCleaningMaxProcessingDurationInMinutes?: number; }; }; }; -- cgit v1.2.3-freya From b2b07e5f21f10faa59ce60bec788306438415b65 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:36:25 +0900 Subject: enhance(backend): 連合関係のサーバー設定のデフォルト値をウィザード側に移動 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - サーバー初期設定ウィザードでデフォルト値を設定できるため、データベース上のデフォルト値でオンにしておく必要がない - 連合は初期設定が終わるまで閉じられている方が安全 --- CHANGELOG.md | 2 +- .../1754019326356-tweakDefaultFederationSettings.js | 18 ++++++++++++++++++ packages/backend/src/models/Meta.ts | 4 ++-- .../frontend/src/components/MkServerSetupWizard.vue | 12 +++++------- 4 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 packages/backend/migration/1754019326356-tweakDefaultFederationSettings.js (limited to 'packages/frontend/src') diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ddad2ec2f..ab8a93d873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - ノートを削除した際、関連するノートが同時に削除されないようになりました - APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります - 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning) - - **デフォルトでオン**になっています + - 既存のサーバーでは**デフォルトでオフ**、新規サーバーでは**デフォルトでオン**になります - データベースの肥大化を防止することが可能です ### Client diff --git a/packages/backend/migration/1754019326356-tweakDefaultFederationSettings.js b/packages/backend/migration/1754019326356-tweakDefaultFederationSettings.js new file mode 100644 index 0000000000..12c723f80d --- /dev/null +++ b/packages/backend/migration/1754019326356-tweakDefaultFederationSettings.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class TweakDefaultFederationSettings1754019326356 { + name = 'TweakDefaultFederationSettings1754019326356' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'none'`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "enableRemoteNotesCleaning" SET DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "enableRemoteNotesCleaning" SET DEFAULT true`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'all'`); + } +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index c97fcd8dfc..1fc50cbd07 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -654,7 +654,7 @@ export class MiMeta { @Column('varchar', { length: 128, - default: 'all', + default: 'none', }) public federation: 'all' | 'specified' | 'none'; @@ -703,7 +703,7 @@ export class MiMeta { public allowExternalApRedirect: boolean; @Column('boolean', { - default: true, + default: false, }) public enableRemoteNotesCleaning: boolean; diff --git a/packages/frontend/src/components/MkServerSetupWizard.vue b/packages/frontend/src/components/MkServerSetupWizard.vue index 23e0e85bc9..d2f56b55c4 100644 --- a/packages/frontend/src/components/MkServerSetupWizard.vue +++ b/packages/frontend/src/components/MkServerSetupWizard.vue @@ -207,15 +207,13 @@ const props = withDefaults(defineProps<{ }>(), { }); -const currentMeta = await misskeyApi('admin/meta'); - -const q_name = ref(currentMeta.name ?? ''); +const q_name = ref(''); const q_use = ref('single'); const q_scale = ref('small'); -const q_federation = ref(currentMeta.federation === 'none' ? 'no' : 'yes'); -const q_remoteContentsCleaning = ref(currentMeta.enableRemoteNotesCleaning); -const q_adminName = ref(currentMeta.maintainerName ?? ''); -const q_adminEmail = ref(currentMeta.maintainerEmail ?? ''); +const q_federation = ref('yes'); +const q_remoteContentsCleaning = ref(true); +const q_adminName = ref(''); +const q_adminEmail = ref(''); const serverSettings = computed(() => { let enableReactionsBuffering; -- cgit v1.2.3-freya From 1082145c749dd2812dd89ca4ad323d6591ebac49 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:54:33 +0900 Subject: enhance: ジョブのログを表示できるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/QueueService.ts | 7 ++ packages/backend/src/server/api/endpoint-list.ts | 1 + .../api/endpoints/admin/queue/show-job-logs.ts | 45 +++++++++++++ .../frontend/src/pages/admin/job-queue.job.vue | 9 +++ packages/misskey-js/etc/misskey-js.api.md | 8 +++ packages/misskey-js/src/autogen/apiClientJSDoc.ts | 11 ++++ packages/misskey-js/src/autogen/endpoint.ts | 3 + packages/misskey-js/src/autogen/entities.ts | 2 + packages/misskey-js/src/autogen/types.ts | 76 ++++++++++++++++++++++ 9 files changed, 162 insertions(+) create mode 100644 packages/backend/src/server/api/endpoints/admin/queue/show-job-logs.ts (limited to 'packages/frontend/src') diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 06170b242a..4be568b334 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -810,6 +810,13 @@ export class QueueService { } } + @bindThis + public async queueGetJobLogs(queueType: typeof QUEUE_TYPES[number], jobId: string) { + const queue = this.getQueue(queueType); + const result = await queue.getJobLogs(jobId); + return result.logs; + } + @bindThis public async queueGetJobs(queueType: typeof QUEUE_TYPES[number], jobTypes: JobType[], search?: string) { const RETURN_LIMIT = 100; diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 5c4a58a6fc..c0c43dd5c9 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -70,6 +70,7 @@ export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-dela export * as 'admin/queue/retry-job' from './endpoints/admin/queue/retry-job.js'; export * as 'admin/queue/remove-job' from './endpoints/admin/queue/remove-job.js'; export * as 'admin/queue/show-job' from './endpoints/admin/queue/show-job.js'; +export * as 'admin/queue/show-job-logs' from './endpoints/admin/queue/show-job-logs.js'; export * as 'admin/queue/promote-jobs' from './endpoints/admin/queue/promote-jobs.js'; export * as 'admin/queue/jobs' from './endpoints/admin/queue/jobs.js'; export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/queue/show-job-logs.ts b/packages/backend/src/server/api/endpoints/admin/queue/show-job-logs.ts new file mode 100644 index 0000000000..b9292ed12a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/queue/show-job-logs.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'read:admin:queue', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + optional: false, nullable: false, + type: 'string', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + queue: { type: 'string', enum: QUEUE_TYPES }, + jobId: { type: 'string' }, + }, + required: ['queue', 'jobId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + return this.queueService.queueGetJobLogs(ps.queue, ps.jobId); + }); + } +} diff --git a/packages/frontend/src/pages/admin/job-queue.job.vue b/packages/frontend/src/pages/admin/job-queue.job.vue index 4ecdb74199..f96830e57a 100644 --- a/packages/frontend/src/pages/admin/job-queue.job.vue +++ b/packages/frontend/src/pages/admin/job-queue.job.vue @@ -155,6 +155,10 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ Load logs +
{{ log }}
+
@@ -198,6 +202,7 @@ const emit = defineEmits<{ const tab = ref('info'); const editData = ref(JSON5.stringify(props.job.data, null, '\t')); const canEdit = true; +const logs = ref([]); type TlType = TlEvent<{ type: 'created' | 'processed' | 'finished'; @@ -268,6 +273,10 @@ async function removeJob() { os.apiWithDialog('admin/queue/remove-job', { queue: props.queueType, jobId: props.job.id }); } +async function loadLogs() { + logs.value = await os.apiWithDialog('admin/queue/show-job-logs', { queue: props.queueType, jobId: props.job.id }); +} + // TODO // function moveJob() { // diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 4ed1c3629f..ae12547f35 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -296,6 +296,12 @@ type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requ // @public (undocumented) type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json']; +// @public (undocumented) +type AdminQueueShowJobLogsRequest = operations['admin___queue___show-job-logs']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminQueueShowJobLogsResponse = operations['admin___queue___show-job-logs']['responses']['200']['content']['application/json']; + // @public (undocumented) type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json']; @@ -1559,6 +1565,8 @@ declare namespace entities { AdminQueueRetryJobRequest, AdminQueueShowJobRequest, AdminQueueShowJobResponse, + AdminQueueShowJobLogsRequest, + AdminQueueShowJobLogsResponse, AdminQueueStatsResponse, AdminRelaysAddRequest, AdminRelaysAddResponse, diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 4a13045592..5407b7a653 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -713,6 +713,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 5ef493946c..d7cb2a46eb 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -88,6 +88,8 @@ import type { AdminQueueRetryJobRequest, AdminQueueShowJobRequest, AdminQueueShowJobResponse, + AdminQueueShowJobLogsRequest, + AdminQueueShowJobLogsResponse, AdminQueueStatsResponse, AdminRelaysAddRequest, AdminRelaysAddResponse, @@ -717,6 +719,7 @@ export type Endpoints = { 'admin/queue/remove-job': { req: AdminQueueRemoveJobRequest; res: EmptyResponse }; 'admin/queue/retry-job': { req: AdminQueueRetryJobRequest; res: EmptyResponse }; 'admin/queue/show-job': { req: AdminQueueShowJobRequest; res: AdminQueueShowJobResponse }; + 'admin/queue/show-job-logs': { req: AdminQueueShowJobLogsRequest; res: AdminQueueShowJobLogsResponse }; 'admin/queue/stats': { req: EmptyRequest; res: AdminQueueStatsResponse }; 'admin/relays/add': { req: AdminRelaysAddRequest; res: AdminRelaysAddResponse }; 'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index a11bbefde5..a14febb6e6 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -91,6 +91,8 @@ export type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job' export type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json']; export type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json']; export type AdminQueueShowJobResponse = operations['admin___queue___show-job']['responses']['200']['content']['application/json']; +export type AdminQueueShowJobLogsRequest = operations['admin___queue___show-job-logs']['requestBody']['content']['application/json']; +export type AdminQueueShowJobLogsResponse = operations['admin___queue___show-job-logs']['responses']['200']['content']['application/json']; export type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json']; export type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json']; export type AdminRelaysAddResponse = operations['admin___relays___add']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index c0a6dca67e..50a96174c7 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -584,6 +584,15 @@ export type paths = { */ post: operations['admin___queue___show-job']; }; + '/admin/queue/show-job-logs': { + /** + * admin/queue/show-job-logs + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:queue* + */ + post: operations['admin___queue___show-job-logs']; + }; '/admin/queue/stats': { /** * admin/queue/stats @@ -10167,6 +10176,73 @@ export interface operations { }; }; }; + 'admin___queue___show-job-logs': { + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + jobId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': string[]; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; admin___queue___stats: { responses: { /** @description OK (with results) */ -- cgit v1.2.3-freya From 62f68de8006e015eec2cee97b31c66413a501a11 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:31:49 +0900 Subject: fix(frontend); Playのボタンがはみ出している問題を修正 (#16303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/pages/flash/flash.vue | 1 + 1 file changed, 1 insertion(+) (limited to 'packages/frontend/src') diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index 1c9cb92bc2..8443293d34 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -366,6 +366,7 @@ definePage(() => ({ > .items { display: flex; + flex-wrap: wrap; justify-content: center; gap: 12px; padding: 16px; -- cgit v1.2.3-freya From e092008dc5768cb57b9eeb2ff70e5b831e0dfa24 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:20:40 +0900 Subject: feat(frontend): セーフモード (#16245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(frontend): セーフモード * Update Changelog * Update Changelog * fix * fix * Update Changelog * Update Changelog * PWAのショートカット経由でもセーフモードで起動できるように * Update ClientServerService.ts --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 6 ++ locales/index.d.ts | 20 +++++++ locales/ja-JP.yml | 5 ++ .../backend/src/server/web/ClientServerService.ts | 4 ++ packages/backend/src/server/web/boot.js | 58 +++++++++++++------ packages/backend/src/server/web/manifest.json | 8 ++- packages/frontend-shared/js/config.ts | 1 + packages/frontend/src/boot/common.ts | 65 ++++++++++++---------- packages/frontend/src/boot/main-boot.ts | 22 +++++++- packages/frontend/src/local-storage.ts | 1 + .../frontend/src/pages/settings/custom-css.vue | 3 + packages/frontend/src/pages/settings/plugin.vue | 6 +- packages/frontend/src/pages/settings/theme.vue | 6 +- packages/frontend/src/plugin.ts | 2 + packages/frontend/src/ui/_common_/common.vue | 22 +++++++- 15 files changed, 180 insertions(+), 49 deletions(-) (limited to 'packages/frontend/src') diff --git a/CHANGELOG.md b/CHANGELOG.md index f8e961073c..56efd7477c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ - 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました ### Client +- Feat: セーフモード + - プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます + - 以下の方法でセーフモードを起動できます + - `g` キーを連打する + - URLに`?safemode=true`を付ける + - PWAのショートカットで Safemode を選択して起動する - Fix: 一部の設定検索結果が存在しないパスになる問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171) - Fix: テーマエディタが動作しない問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index d2e2b729e8..f77925b410 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5501,6 +5501,22 @@ export interface Locale extends ILocale { * 日 */ "inDays": string; + /** + * セーフモードが有効です + */ + "safeModeEnabled": string; + /** + * セーフモードが有効なため、プラグインはすべて無効化されています。 + */ + "pluginsAreDisabledBecauseSafeMode": string; + /** + * セーフモードが有効なため、カスタムCSSは適用されていません。 + */ + "customCssIsDisabledBecauseSafeMode": string; + /** + * セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。 + */ + "themeIsDefaultBecauseSafeMode": string; "_order": { /** * 新しい順 @@ -11839,6 +11855,10 @@ export interface Locale extends ILocale { * 修復ツールを起動 */ "otherOption3": string; + /** + * Misskeyをセーフモードで起動 + */ + "otherOption4": string; }; "_search": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 377231ee19..4d79b31b1b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1370,6 +1370,10 @@ defaultImageCompressionLevel: "デフォルトの画像圧縮度" defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。" inMinutes: "分" inDays: "日" +safeModeEnabled: "セーフモードが有効です" +pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プラグインはすべて無効化されています。" +customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。" +themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。" _order: newest: "新しい順" @@ -3164,6 +3168,7 @@ _bootErrors: otherOption1: "クライアント設定とキャッシュを削除" otherOption2: "簡易クライアントを起動" otherOption3: "修復ツールを起動" + otherOption4: "Misskeyをセーフモードで起動" _search: searchScopeAll: "全て" diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 4d122b0fcf..768cfde701 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -188,6 +188,10 @@ export class ClientServerService { 'url': 'url', }, }, + 'shortcuts': [{ + 'name': 'Safemode', + 'url': '/?safemode=true', + }], }; manifest = { diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 24794cbf2a..1a30e9ed2b 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -94,23 +94,37 @@ } //#endregion + let isSafeMode = (localStorage.getItem('isSafeMode') === 'true'); + + if (!isSafeMode) { + const urlParams = new URLSearchParams(window.location.search); + + if (urlParams.has('safemode') && urlParams.get('safemode') === 'true') { + localStorage.setItem('isSafeMode', 'true'); + isSafeMode = true; + } + } + //#region Theme - const theme = localStorage.getItem('theme'); - if (theme) { - for (const [k, v] of Object.entries(JSON.parse(theme))) { - document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); - - // HTMLの theme-color 適用 - if (k === 'htmlThemeColor') { - for (const tag of document.head.children) { - if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { - tag.setAttribute('content', v); - break; + if (!isSafeMode) { + const theme = localStorage.getItem('theme'); + if (theme) { + for (const [k, v] of Object.entries(JSON.parse(theme))) { + document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); + + // HTMLの theme-color 適用 + if (k === 'htmlThemeColor') { + for (const tag of document.head.children) { + if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { + tag.setAttribute('content', v); + break; + } } } } } } + const colorScheme = localStorage.getItem('colorScheme'); if (colorScheme) { document.documentElement.style.setProperty('color-scheme', colorScheme); @@ -127,11 +141,13 @@ document.documentElement.classList.add('useSystemFont'); } - const customCss = localStorage.getItem('customCss'); - if (customCss && customCss.length > 0) { - const style = document.createElement('style'); - style.innerHTML = customCss; - document.head.appendChild(style); + if (!isSafeMode) { + const customCss = localStorage.getItem('customCss'); + if (customCss && customCss.length > 0) { + const style = document.createElement('style'); + style.innerHTML = customCss; + document.head.appendChild(style); + } } async function addStyle(styleText) { @@ -159,9 +175,13 @@ otherOption1: 'Clear preferences and cache', otherOption2: 'Start the simple client', otherOption3: 'Start the repair tool', + otherOption4: 'Start Misskey in safe mode', }, locale?._bootErrors || {}); const reload = locale?.reload || 'Reload'; + const safeModeUrl = new URL(window.location.href); + safeModeUrl.searchParams.set('safemode', 'true'); + let errorsElement = document.getElementById('errors'); if (!errorsElement) { @@ -182,6 +202,12 @@

${messages.solution4}

${messages.otherOption} + + + +