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;