diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2025-02-27 11:56:17 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-02-27 02:56:17 +0000 |
| commit | da66079c297c18f5628d262bf5d26662e5c25011 (patch) | |
| tree | 95024635e7780791c736b571a6db150cd8e01ed2 /packages/frontend | |
| parent | fix(frontend): MkSelectの初期値が表示されない場合がある (#15559) (diff) | |
| download | sharkey-da66079c297c18f5628d262bf5d26662e5c25011.tar.gz sharkey-da66079c297c18f5628d262bf5d26662e5c25011.tar.bz2 sharkey-da66079c297c18f5628d262bf5d26662e5c25011.zip | |
enhance(frontend): ノート検索ページのデザイン調整 (#14780)
* enhance(frontend): 検索ページのホスト指定とユーザー指定を統合する (#273)
(cherry picked from commit c79392c88d6bf58ede39d8bba9ca2778c58521ef)
* fix
* :art:
* Update Changelog
* Update Changelog
* refactor
---------
Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
Diffstat (limited to 'packages/frontend')
| -rw-r--r-- | packages/frontend/src/pages/search.note.vue | 315 | ||||
| -rw-r--r-- | packages/frontend/src/pages/search.user.vue | 11 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/gen-search-query.ts | 35 |
3 files changed, 222 insertions, 139 deletions
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index 14b9f7a741..a390e3fba1 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -6,69 +6,127 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps"> <div class="_gaps"> - <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search"> + <MkInput + v-model="searchQuery" + large + autofocus + type="search" + @enter.prevent="search" + > <template #prefix><i class="ti ti-search"></i></template> </MkInput> - <MkFoldableSection :expanded="true"> + <MkFoldableSection expanded> <template #header>{{ i18n.ts.options }}</template> <div class="_gaps_m"> - <template v-if="instance.federation !== 'none'"> - <MkRadios v-model="hostSelect"> - <template #label>{{ i18n.ts.host }}</template> - <option value="all" default>{{ i18n.ts.all }}</option> - <option value="local">{{ i18n.ts.local }}</option> - <option v-if="noteSearchableScope === 'global'" value="specified">{{ i18n.ts.specifyHost }}</option> - </MkRadios> - <MkInput v-if="noteSearchableScope === 'global'" v-model="hostInput" :disabled="hostSelect !== 'specified'" :large="true" type="search"> + <MkRadios v-model="searchScope"> + <option v-if="instance.federation !== 'none' && noteSearchableScope === 'global'" value="all">{{ i18n.ts._search.searchScopeAll }}</option> + <option value="local">{{ instance.federation === 'none' ? i18n.ts._search.searchScopeAll : i18n.ts._search.searchScopeLocal }}</option> + <option v-if="instance.federation !== 'none' && noteSearchableScope === 'global'" value="server">{{ i18n.ts._search.searchScopeServer }}</option> + <option value="user">{{ i18n.ts._search.searchScopeUser }}</option> + </MkRadios> + + <div v-if="instance.federation !== 'none' && searchScope === 'server'" :class="$style.subOptionRoot"> + <MkInput + v-model="hostInput" + :placeholder="i18n.ts._search.serverHostPlaceholder" + @enter.prevent="search" + > + <template #label>{{ i18n.ts._search.pleaseEnterServerHost }}</template> <template #prefix><i class="ti ti-server"></i></template> </MkInput> - </template> - - <MkFolder :defaultOpen="true"> - <template #label>{{ i18n.ts.specifyUser }}</template> - <template v-if="user" #suffix>@{{ user.username }}{{ user.host ? `@${user.host}` : "" }}</template> + </div> + <div v-if="searchScope === 'user'" :class="$style.subOptionRoot"> + <div :class="$style.userSelectLabel">{{ i18n.ts._search.pleaseSelectUser }}</div> <div class="_gaps"> - <div :class="$style.userItem"> - <MkUserCardMini v-if="user" :class="$style.userCard" :user="user" :withChart="false"/> - <MkButton v-if="user == null && $i != null" transparent :class="$style.addMeButton" @click="selectSelf"><div :class="$style.addUserButtonInner"><span><i class="ti ti-plus"></i><i class="ti ti-user"></i></span><span>{{ i18n.ts.selectSelf }}</span></div></MkButton> - <MkButton v-if="user == null" transparent :class="$style.addUserButton" @click="selectUser"><div :class="$style.addUserButtonInner"><i class="ti ti-plus"></i><span>{{ i18n.ts.selectUser }}</span></div></MkButton> - <button class="_button" :class="$style.remove" :disabled="user == null" @click="removeUser"><i class="ti ti-x"></i></button> + <div v-if="user == null" :class="$style.userSelectButtons"> + <div v-if="$i != null"> + <MkButton + transparent + :class="$style.userSelectButton" + @click="selectSelf" + > + <div :class="$style.userSelectButtonInner"> + <span><i class="ti ti-plus"></i><i class="ti ti-user"></i></span> + <span>{{ i18n.ts.selectSelf }}</span> + </div> + </MkButton> + </div> + <div :style="$i == null ? 'grid-column: span 2;' : undefined"> + <MkButton + transparent + :class="$style.userSelectButton" + @click="selectUser" + > + <div :class="$style.userSelectButtonInner"> + <span><i class="ti ti-plus"></i></span> + <span>{{ i18n.ts.selectUser }}</span> + </div> + </MkButton> + </div> + </div> + <div v-else :class="$style.userSelectedButtons"> + <div style="overflow: hidden;"> + <MkUserCardMini + :user="user" + :withChart="false" + :class="$style.userSelectedCard" + /> + </div> + <div> + <button + class="_button" + :class="$style.userSelectedRemoveButton" + @click="removeUser" + > + <i class="ti ti-x"></i> + </button> + </div> </div> </div> - </MkFolder> + </div> </div> </MkFoldableSection> <div> - <MkButton large primary gradate rounded style="margin: 0 auto;" @click="search">{{ i18n.ts.search }}</MkButton> + <MkButton + large + primary + gradate + rounded + :disabled="searchParams == null" + style="margin: 0 auto;" + @click="search" + > + {{ i18n.ts.search }} + </MkButton> </div> </div> <MkFoldableSection v-if="notePagination"> <template #header>{{ i18n.ts.searchResult }}</template> - <MkNotes :key="key" :pagination="notePagination"/> + <MkNotes :key="`searchNotes:${key}`" :pagination="notePagination"/> </MkFoldableSection> </div> </template> <script lang="ts" setup> -import { computed, ref, toRef, watch } from 'vue'; -import type { UserDetailed } from 'misskey-js/entities.js'; +import { computed, ref, shallowRef, toRef } from 'vue'; +import type * as Misskey from 'misskey-js'; import type { Paging } from '@/components/MkPagination.vue'; -import MkNotes from '@/components/MkNotes.vue'; -import MkInput from '@/components/MkInput.vue'; -import MkButton from '@/components/MkButton.vue'; +import { $i } from '@/account.js'; +import { host as localHost } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; +import { instance } from '@/instance.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import MkFoldableSection from '@/components/MkFoldableSection.vue'; -import MkFolder from '@/components/MkFolder.vue'; import { useRouter } from '@/router/supplier.js'; -import MkUserCardMini from '@/components/MkUserCardMini.vue'; +import MkButton from '@/components/MkButton.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkNotes from '@/components/MkNotes.vue'; import MkRadios from '@/components/MkRadios.vue'; -import { $i } from '@/account.js'; -import { instance } from '@/instance.js'; +import MkUserCardMini from '@/components/MkUserCardMini.vue'; const props = withDefaults(defineProps<{ query?: string; @@ -83,76 +141,127 @@ const props = withDefaults(defineProps<{ }); const router = useRouter(); + const key = ref(0); +const notePagination = ref<Paging<'notes/search'>>(); + const searchQuery = ref(toRef(props, 'query').value); -const notePagination = ref<Paging>(); -const user = ref<UserDetailed | null>(null); const hostInput = ref(toRef(props, 'host').value); +const user = shallowRef<Misskey.entities.UserDetailed | null>(null); + +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const noteSearchableScope = instance.noteSearchableScope ?? 'local'; -const hostSelect = ref<'all' | 'local' | 'specified'>('all'); +//#region set user +let fetchedUser: Misskey.entities.UserDetailed | null = null; -const setHostSelectWithInput = (after: string | undefined | null, before: string | undefined | null) => { - if (before === after) return; - if (after === '') hostSelect.value = 'all'; - else hostSelect.value = 'specified'; +if (props.userId) { + fetchedUser = await misskeyApi('users/show', { + userId: props.userId, + }).catch(() => null); +} + +if (props.username && fetchedUser == null) { + fetchedUser = await misskeyApi('users/show', { + username: props.username, + ...(props.host ? { host: props.host } : {}), + }).catch(() => null); +} + +if (fetchedUser != null) { + if (!(noteSearchableScope === 'local' && fetchedUser.host != null)) { + user.value = fetchedUser; + } +} +//#endregion + +const searchScope = ref<'all' | 'local' | 'server' | 'user'>((() => { + if (user.value != null) return 'user'; + if (noteSearchableScope === 'local') return 'local'; + if (hostInput.value) return 'server'; + return 'all'; +})()); + +type SearchParams = { + readonly query: string; + readonly host?: string; + readonly userId?: string; }; -setHostSelectWithInput(hostInput.value, undefined); +const fixHostIfLocal = (target: string | null | undefined) => { + if (!target || target === localHost) return '.'; + return target; +}; -watch(hostInput, setHostSelectWithInput); +const searchParams = computed<SearchParams | null>(() => { + const trimmedQuery = searchQuery.value.trim(); + if (!trimmedQuery) return null; -const searchHost = computed(() => { - if (hostSelect.value === 'local' || instance.federation === 'none') return '.'; - if (hostSelect.value === 'specified') return hostInput.value; - return null; -}); + if (searchScope.value === 'user') { + if (user.value == null) return null; + return { + query: trimmedQuery, + host: fixHostIfLocal(user.value.host), + userId: user.value.id, + }; + } -if (props.userId != null) { - misskeyApi('users/show', { userId: props.userId }).then(_user => { - user.value = _user; - }); -} else if (props.username != null) { - misskeyApi('users/show', { - username: props.username, - ...(props.host != null && props.host !== '') ? { host: props.host } : {}, - }).then(_user => { - user.value = _user; - }); -} + if (instance.federation !== 'none' && searchScope.value === 'server') { + let trimmedHost = hostInput.value?.trim(); + if (!trimmedHost) return null; + if (trimmedHost.startsWith('https://') || trimmedHost.startsWith('http://')) { + try { + trimmedHost = new URL(trimmedHost).host; + } catch (err) { /* empty */ } + } + return { + query: trimmedQuery, + host: fixHostIfLocal(trimmedHost), + }; + } + + if (instance.federation === 'none' || searchScope.value === 'local') { + return { + query: trimmedQuery, + host: '.', + }; + } + + return { + query: trimmedQuery, + }; +}); function selectUser() { - os.selectUser({ includeSelf: true, localOnly: instance.noteSearchableScope === 'local' }).then(_user => { + os.selectUser({ + includeSelf: true, + localOnly: instance.noteSearchableScope === 'local', + }).then(_user => { user.value = _user; - hostInput.value = _user.host ?? ''; }); } function selectSelf() { - user.value = $i as UserDetailed | null; - hostInput.value = null; + user.value = $i; } function removeUser() { user.value = null; - hostInput.value = ''; } async function search() { - const query = searchQuery.value.toString().trim(); - - if (query == null || query === '') return; + if (searchParams.value == null) return; //#region AP lookup - if (query.startsWith('https://') && !query.includes(' ')) { + if (searchParams.value.query.startsWith('https://') && !searchParams.value.query.includes(' ')) { const confirm = await os.confirm({ type: 'info', text: i18n.ts.lookupConfirm, }); if (!confirm.canceled) { const promise = misskeyApi('ap/show', { - uri: query, + uri: searchParams.value.query, }); os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); @@ -161,6 +270,7 @@ async function search() { if (res.type === 'User') { router.push(`/@${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}`); } @@ -170,25 +280,25 @@ async function search() { } //#endregion - if (query.length > 1 && !query.includes(' ')) { - if (query.startsWith('@')) { + if (searchParams.value.query.length > 1 && !searchParams.value.query.includes(' ')) { + if (searchParams.value.query.startsWith('@')) { const confirm = await os.confirm({ type: 'info', text: i18n.ts.lookupConfirm, }); if (!confirm.canceled) { - router.push(`/${query}`); + router.push(`/${searchParams.value.query}`); return; } } - if (query.startsWith('#')) { + if (searchParams.value.query.startsWith('#')) { const confirm = await os.confirm({ type: 'info', text: i18n.ts.openTagPageConfirm, }); if (!confirm.canceled) { - router.push(`/tags/${encodeURIComponent(query.substring(1))}`); + router.push(`/tags/${encodeURIComponent(searchParams.value.query.substring(1))}`); return; } } @@ -198,9 +308,7 @@ async function search() { endpoint: 'notes/search', limit: 10, params: { - query: searchQuery.value, - userId: user.value ? user.value.id : null, - ...(searchHost.value ? { host: searchHost.value } : {}), + ...searchParams.value, }, }; @@ -208,41 +316,48 @@ async function search() { } </script> <style lang="scss" module> -.userItem { - display: flex; - justify-content: center; +.subOptionRoot { + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); + padding: var(--MI-margin); } -.addMeButton { - border: 2px dashed var(--MI_THEME-fgTransparent); - padding: 12px; - margin-right: 16px; + +.userSelectLabel { + font-size: 0.85em; + padding: 0 0 8px; + user-select: none; } -.addUserButton { - border: 2px dashed var(--MI_THEME-fgTransparent); + +.userSelectButtons { + display: grid; + grid-template-columns: auto 1fr; + gap: 16px; +} + +.userSelectButton { + width: 100%; + height: 100%; padding: 12px; - flex-grow: 1; + border: 2px dashed var(--MI_THEME-fgTransparent); } -.addUserButtonInner { + +.userSelectButtonInner { display: flex; flex-direction: column; align-items: center; justify-content: space-between; min-height: 38px; } -.userCard { - flex-grow: 1; + +.userSelectedButtons { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; } -.remove { + +.userSelectedRemoveButton { width: 32px; height: 32px; - align-self: center; - - & > i:before { - color: #ff2a2a; - } - - &:disabled { - opacity: 0; - } + color: #ff2a2a; } </style> diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index e8bc4cd6d3..2b8faf5465 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFoldableSection v-if="userPagination"> <template #header>{{ i18n.ts.searchResult }}</template> - <MkUserList :key="key" :pagination="userPagination"/> + <MkUserList :key="`searchUsers:${key}`" :pagination="userPagination"/> </MkFoldableSection> </div> </template> @@ -49,14 +49,16 @@ const props = withDefaults(defineProps<{ const router = useRouter(); -const key = ref(''); +const key = ref(0); +const userPagination = ref<Paging<'users/search'>>(); + const searchQuery = ref(toRef(props, 'query').value); const searchOrigin = ref(toRef(props, 'origin').value); -const userPagination = ref<Paging>(); async function search() { const query = searchQuery.value.toString().trim(); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (query == null || query === '') return; //#region AP lookup @@ -76,6 +78,7 @@ async function search() { if (res.type === 'User') { router.push(`/@${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}`); } @@ -118,6 +121,6 @@ async function search() { }, }; - key.value = query; + key.value++; } </script> diff --git a/packages/frontend/src/scripts/gen-search-query.ts b/packages/frontend/src/scripts/gen-search-query.ts deleted file mode 100644 index a85ee01e26..0000000000 --- a/packages/frontend/src/scripts/gen-search-query.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import { host as localHost } from '@@/js/config.js'; - -export async function genSearchQuery(v: any, q: string) { - let host: string; - let userId: string; - if (q.split(' ').some(x => x.startsWith('@'))) { - for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substring(1))) { - if (at.includes('.')) { - if (at === localHost || at === '.') { - host = null; - } else { - host = at; - } - } else { - const user = await v.api('users/show', Misskey.acct.parse(at)).catch(x => null); - if (user) { - userId = user.id; - } else { - // todo: show error - } - } - } - } - return { - query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '), - host: host, - userId: userId, - }; -} |