summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authortaichan <40626578+tai-cha@users.noreply.github.com>2024-07-30 15:51:08 +0900
committerGitHub <noreply@github.com>2024-07-30 15:51:08 +0900
commitbff813042e670dcb59a6b0dcc334ca44f880752b (patch)
treefcdceb7d18a5b3a1f87083b2c062be6da8281866 /packages
parentFix(backend): ドライブのファイルのurl, uri, src の上限引き上... (diff)
downloadsharkey-bff813042e670dcb59a6b0dcc334ca44f880752b.tar.gz
sharkey-bff813042e670dcb59a6b0dcc334ca44f880752b.tar.bz2
sharkey-bff813042e670dcb59a6b0dcc334ca44f880752b.zip
feat: このユーザーのノートを検索, クエリに基づく検索の初期値 & ノート検索のUI改善 (#14128)
* refactor(frontend): noteSearchAvailableをaccountsに移動 * feat: searchページでのクエリの受取りとtypeによる表示タブの変更 * user検索でsearchの親から受け取った値を基に入力値を初期化 * feat(frontend): ノート検索で親(search)からの情報を基にユーザー情報を取得 * feat(frontend): ユーザーのノートを検索するページに遷移するボタン * feat(frontend): ノート検索にホスト名指定のオプション追加 also :art: * style: ただ照会部分を囲っただけ(可読性確保のために) * refactor: remove unneed import defineProps and withDefaults are compiler micro when using `<script setup>` FYI: https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits:~:text=defineProps%20and%20defineEmits%20are%20compiler%20macros%20only%20usable%20inside%20%3Cscript%20setup%3E.%20They%20do%20not%20need%20to%20be%20imported%2C%20and%20are%20compiled%20away%20when%20%3Cscript%20setup%3E%20is%20processed. * Update CHANGELOG * Fix: ノート検索の初期値が常にホスト指定になってしまう * notesSearchAvailableをaccountに持たせるのをやめる * SDPX-Licence-Identifier * Fix: Vitest fails due to instance.policies being undefined * Add Storybook for search * Fix(storybook): ノート検索が利用できないと出てしまう問題 * storybookでユーザー選択ができないのを修正 * feat: ノート検索で自分を選択可能に & :art: * feat(background): api/metaで検索可能なノートのスコープを参照できるように * globalのノートが検索不可能な場合、検索オプションを表示しないように * Update CHANGELOG.md * config.meilisearch.scopeがstring[]を取ることがあるので修正 * meilisearchを利用かつscopeがlocalの場合、リモートユーザーのメニューで「このユーザーのノートを検索」を出さないように * hostが空文字の時の挙動を修正 * ローカルのみしかノートがインデックスされていない場合、リモートユーザーも選択できなくした
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/src/core/entities/MetaEntityService.ts1
-rw-r--r--packages/backend/src/models/json-schema/meta.ts6
-rw-r--r--packages/frontend/.storybook/fakes.ts2
-rw-r--r--packages/frontend/.storybook/generate.tsx1
-rw-r--r--packages/frontend/src/components/MkRadios.vue4
-rw-r--r--packages/frontend/src/pages/search.note.vue150
-rw-r--r--packages/frontend/src/pages/search.stories.impl.ts88
-rw-r--r--packages/frontend/src/pages/search.user.vue20
-rw-r--r--packages/frontend/src/pages/search.vue34
-rw-r--r--packages/frontend/src/router/definition.ts3
-rw-r--r--packages/frontend/src/scripts/check-permissions.ts19
-rw-r--r--packages/frontend/src/scripts/get-user-menu.ts10
-rw-r--r--packages/misskey-js/src/autogen/types.ts5
13 files changed, 307 insertions, 36 deletions
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index 09641ce485..bcfd8ae41c 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -128,6 +128,7 @@ export class MetaEntityService {
mediaProxy: this.config.mediaProxy,
enableUrlPreview: instance.urlPreviewEnabled,
+ noteSearchableScope: this.config.meilisearch == null || this.config.meilisearch.scope === 'local' ? 'local' : 'global',
};
return packed;
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index e7bc6356e5..3bcf9cac92 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -247,6 +247,12 @@ export const packedMetaLiteSchema = {
optional: false, nullable: false,
ref: 'RolePolicies',
},
+ noteSearchableScope: {
+ type: 'string',
+ enum: ['local', 'global'],
+ optional: false, nullable: false,
+ default: 'local',
+ },
},
} as const;
diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts
index 9d789a34ff..01f1046609 100644
--- a/packages/frontend/.storybook/fakes.ts
+++ b/packages/frontend/.storybook/fakes.ts
@@ -154,7 +154,7 @@ export function federationInstance(): entities.FederationInstance {
};
}
-export function userDetailed(id = 'someuserid', username = 'miskist', host = 'misskey-hub.net', name = 'Misskey User'): entities.UserDetailed {
+export function userDetailed(id = 'someuserid', username = 'miskist', host:entities.UserDetailed['host'] = 'misskey-hub.net', name:entities.UserDetailed['name'] = 'Misskey User'): entities.UserDetailed {
return {
id,
username,
diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index 7b6c86447e..b94dfc53e3 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -405,6 +405,7 @@ function toStories(component: string): Promise<string> {
glob('src/components/MkUserSetupDialog.*.vue'),
glob('src/components/MkInstanceCardMini.vue'),
glob('src/components/MkInviteCode.vue'),
+ glob('src/pages/search.vue'),
glob('src/pages/user/home.vue'),
]);
const components = globs.flat();
diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue
index 549438f61b..705c93f770 100644
--- a/packages/frontend/src/components/MkRadios.vue
+++ b/packages/frontend/src/components/MkRadios.vue
@@ -29,6 +29,9 @@ export default defineComponent({
// なぜかFragmentになることがあるため
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
+ // vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる)
+ options = options.filter(vnode => !(typeof vnode.type === 'symbol' && vnode.type.description === 'v-cmt' && vnode.children === 'v-if'));
+
return () => h('div', {
class: 'novjtcto',
}, [
@@ -40,6 +43,7 @@ export default defineComponent({
}, options.map(option => h(MkRadio, {
key: option.key as string,
value: option.props?.value,
+ disabled: option.props?.disabled,
modelValue: value.value,
'onUpdate:modelValue': _v => value.value = _v,
}, () => option.children)),
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index d68bbaeeca..05dc77b94b 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -9,26 +9,35 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
- <MkFolder>
- <template #label>{{ i18n.ts.options }}</template>
+ <MkFoldableSection :expanded="true">
+ <template #header>{{ i18n.ts.options }}</template>
<div class="_gaps_m">
- <MkSwitch v-model="isLocalOnly">{{ i18n.ts.localOnly }}</MkSwitch>
+ <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">
+ <template #prefix><i class="ti ti-server"></i></template>
+ </MkInput>
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.specifyUser }}</template>
- <template v-if="user" #suffix>@{{ user.username }}</template>
+ <template v-if="user" #suffix>@{{ user.username }}{{ user.host ? `@${user.host}` : "" }}</template>
- <div style="text-align: center;" class="_gaps">
- <div v-if="user">@{{ user.username }}</div>
- <div>
- <MkButton v-if="user == null" primary rounded inline @click="selectUser">{{ i18n.ts.selectUser }}</MkButton>
- <MkButton v-else danger rounded inline @click="user = null">{{ i18n.ts.remove }}</MkButton>
+ <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>
</div>
</MkFolder>
</div>
- </MkFolder>
+ </MkFoldableSection>
<div>
<MkButton large primary gradate rounded style="margin: 0 auto;" @click="search">{{ i18n.ts.search }}</MkButton>
</div>
@@ -42,38 +51,98 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { computed, ref, toRef, watch } from 'vue';
+import type { UserDetailed } from 'misskey-js/entities.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 MkSwitch from '@/components/MkSwitch.vue';
import { i18n } from '@/i18n.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 MkRadios from '@/components/MkRadios.vue';
+import { $i } from '@/account.js';
+import { instance } from '@/instance.js';
-const router = useRouter();
+const props = withDefaults(defineProps<{
+ query?: string;
+ userId?: string;
+ username?: string;
+ host?: string | null;
+}>(), {
+ query: '',
+ userId: undefined,
+ username: undefined,
+ host: '',
+});
+const router = useRouter();
const key = ref(0);
-const searchQuery = ref('');
-const searchOrigin = ref('combined');
-const notePagination = ref();
-const user = ref<any>(null);
-const isLocalOnly = ref(false);
+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 noteSearchableScope = instance.noteSearchableScope ?? 'local';
+
+const hostSelect = ref<'all' | 'local' | 'specified'>('all');
+
+const setHostSelectWithInput = (after:string|undefined|null, before:string|undefined|null) => {
+ if (before === after) return;
+ if (after === '') hostSelect.value = 'all';
+ else hostSelect.value = 'specified';
+};
+
+setHostSelectWithInput(hostInput.value, undefined);
+
+watch(hostInput, setHostSelectWithInput);
+
+const searchHost = computed(() => {
+ if (hostSelect.value === 'local') return '.';
+ if (hostSelect.value === 'specified') return hostInput.value;
+ return null;
+});
+
+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;
+ });
+}
function selectUser() {
- os.selectUser({ includeSelf: true }).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;
+}
+
+function removeUser() {
+ user.value = null;
+ hostInput.value = '';
+}
+
async function search() {
const query = searchQuery.value.toString().trim();
if (query == null || query === '') return;
+ //#region AP lookup
if (query.startsWith('https://')) {
const promise = misskeyApi('ap/show', {
uri: query,
@@ -91,6 +160,7 @@ async function search() {
return;
}
+ //#endregion
notePagination.value = {
endpoint: 'notes/search',
@@ -98,11 +168,49 @@ async function search() {
params: {
query: searchQuery.value,
userId: user.value ? user.value.id : null,
+ ...(searchHost.value ? { host: searchHost.value } : {}),
},
};
- if (isLocalOnly.value) notePagination.value.params.host = '.';
-
key.value++;
}
</script>
+<style lang="scss" module>
+.userItem {
+ display: flex;
+ justify-content: center;
+}
+.addMeButton {
+ border: 2px dashed var(--fgTransparent);
+ padding: 12px;
+ margin-right: 16px;
+}
+.addUserButton {
+ border: 2px dashed var(--fgTransparent);
+ padding: 12px;
+ flex-grow: 1;
+}
+.addUserButtonInner {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 38px;
+}
+.userCard {
+ flex-grow: 1;
+}
+.remove {
+ width: 32px;
+ height: 32px;
+ align-self: center;
+
+ & > i:before {
+ color: #ff2a2a;
+ }
+
+ &:disabled {
+ opacity: 0;
+ }
+}
+</style>
diff --git a/packages/frontend/src/pages/search.stories.impl.ts b/packages/frontend/src/pages/search.stories.impl.ts
new file mode 100644
index 0000000000..0110a7ab8e
--- /dev/null
+++ b/packages/frontend/src/pages/search.stories.impl.ts
@@ -0,0 +1,88 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { StoryObj } from '@storybook/vue3';
+import { HttpResponse, http } from 'msw';
+import search_ from './search.vue';
+import { userDetailed } from '@/../.storybook/fakes.js';
+import { commonHandlers } from '@/../.storybook/mocks.js';
+
+const localUser = userDetailed('someuserid', 'miskist', null, 'Local Misskey User');
+
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ search_,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<search_ v-bind="props" />',
+ };
+ },
+ args: {
+ ignoreNotesSearchAvailable: true,
+ },
+ parameters: {
+ layout: 'fullscreen',
+ msw: {
+ handlers: [
+ ...commonHandlers,
+ http.post('/api/users/show', () => {
+ return HttpResponse.json(userDetailed());
+ }),
+ http.post('/api/users/search', () => {
+ return HttpResponse.json([userDetailed(), localUser]);
+ }),
+ ],
+ },
+ },
+} satisfies StoryObj<typeof search_>;
+
+export const NoteSearchDisabled = {
+ ...Default,
+ args: {},
+} satisfies StoryObj<typeof search_>;
+
+export const WithUsernameLocal = {
+ ...Default,
+
+ args: {
+ ...Default.args,
+ username: localUser.username,
+ host: localUser.host,
+ },
+ parameters: {
+ layout: 'fullscreen',
+ msw: {
+ handlers: [
+ ...commonHandlers,
+ http.post('/api/users/show', () => {
+ return HttpResponse.json(localUser);
+ }),
+ http.post('/api/users/search', () => {
+ return HttpResponse.json([userDetailed(), localUser]);
+ }),
+ ],
+ },
+ },
+} satisfies StoryObj<typeof search_>;
+
+export const WithUserType = {
+ ...Default,
+ args: {
+ type: 'user',
+ },
+} satisfies StoryObj<typeof search_>;
diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue
index b9c2704bc7..85d869d9cb 100644
--- a/packages/frontend/src/pages/search.user.vue
+++ b/packages/frontend/src/pages/search.user.vue
@@ -25,7 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { ref, toRef } from 'vue';
+import type { Endpoints } from 'misskey-js';
+import type { Paging } from '@/components/MkPagination.vue';
import MkUserList from '@/components/MkUserList.vue';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
@@ -36,18 +38,27 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { useRouter } from '@/router/supplier.js';
+const props = withDefaults(defineProps<{
+ query?: string,
+ origin?: Endpoints['users/search']['req']['origin'],
+}>(), {
+ query: '',
+ origin: 'combined',
+});
+
const router = useRouter();
const key = ref('');
-const searchQuery = ref('');
-const searchOrigin = ref('combined');
-const userPagination = ref();
+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();
if (query == null || query === '') return;
+ //#region AP lookup
if (query.startsWith('https://')) {
const promise = misskeyApi('ap/show', {
uri: query,
@@ -65,6 +76,7 @@ async function search() {
return;
}
+ //#endregion
userPagination.value = {
endpoint: 'users/search',
diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue
index a3dcda77be..38d7548fa8 100644
--- a/packages/frontend/src/pages/search.vue
+++ b/packages/frontend/src/pages/search.vue
@@ -9,8 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<MkSpacer v-if="tab === 'note'" key="note" :contentMax="800">
- <div v-if="notesSearchAvailable">
- <XNote/>
+ <div v-if="notesSearchAvailable || ignoreNotesSearchAvailable">
+ <XNote v-bind="props"/>
</div>
<div v-else>
<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
@@ -18,27 +18,43 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSpacer>
<MkSpacer v-else-if="tab === 'user'" key="user" :contentMax="800">
- <XUser/>
+ <XUser v-bind="props"/>
</MkSpacer>
</MkHorizontalSwipe>
</MkStickyContainer>
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, ref } from 'vue';
+import { computed, defineAsyncComponent, ref, toRef } from 'vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { $i } from '@/account.js';
-import { instance } from '@/instance.js';
+import { notesSearchAvailable } from '@/scripts/check-permissions.js';
import MkInfo from '@/components/MkInfo.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
+const props = withDefaults(defineProps<{
+ query?: string,
+ userId?: string,
+ username?: string,
+ host?: string | null,
+ type?: 'note' | 'user',
+ origin?: 'combined' | 'local' | 'remote',
+ // For storybook only
+ ignoreNotesSearchAvailable?: boolean,
+}>(), {
+ query: '',
+ userId: undefined,
+ username: undefined,
+ host: undefined,
+ type: 'note',
+ origin: 'combined',
+ ignoreNotesSearchAvailable: false,
+});
+
const XNote = defineAsyncComponent(() => import('./search.note.vue'));
const XUser = defineAsyncComponent(() => import('./search.user.vue'));
-const tab = ref('note');
-
-const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes));
+const tab = ref(toRef(props, 'type').value);
const headerActions = computed(() => []);
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index f7a219c57e..995a2055b8 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -232,6 +232,9 @@ const routes: RouteDef[] = [{
component: page(() => import('@/pages/search.vue')),
query: {
q: 'query',
+ userId: 'userId',
+ username: 'username',
+ host: 'host',
channel: 'channel',
type: 'type',
origin: 'origin',
diff --git a/packages/frontend/src/scripts/check-permissions.ts b/packages/frontend/src/scripts/check-permissions.ts
new file mode 100644
index 0000000000..ed86529d5b
--- /dev/null
+++ b/packages/frontend/src/scripts/check-permissions.ts
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { instance } from '@/instance.js';
+import { $i } from '@/account.js';
+
+export const notesSearchAvailable = (
+ // FIXME: instance.policies would be null in Vitest
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ ($i == null && instance.policies != null && instance.policies.canSearchNotes) ||
+ ($i != null && $i.policies.canSearchNotes) ||
+ false
+) as boolean;
+
+export const canSearchNonLocalNotes = (
+ instance.noteSearchableScope === 'global'
+);
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index dacafb859f..e9e7ae771d 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -13,6 +13,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore, userActions } from '@/store.js';
import { $i, iAmModerator } from '@/account.js';
+import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js';
import { IRouter } from '@/nirax.js';
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
import { mainRouter } from '@/router/main.js';
@@ -160,7 +161,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
action: () => {
copyToClipboard(`@${user.username}@${user.host ?? host}`);
},
- }, ...(iAmModerator ? [{
+ }, ...( notesSearchAvailable && (user.host == null || canSearchNonLocalNotes) ? [{
+ icon: 'ti ti-search',
+ text: i18n.ts.searchThisUsersNotes,
+ action: () => {
+ router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`);
+ },
+ }] : [])
+ , ...(iAmModerator ? [{
icon: 'ti ti-user-exclamation',
text: i18n.ts.moderation,
action: () => {
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 4cfe92147a..cb7d82d2d8 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4939,6 +4939,11 @@ export type components = {
serverRules: string[];
themeColor: string | null;
policies: components['schemas']['RolePolicies'];
+ /**
+ * @default local
+ * @enum {string}
+ */
+ noteSearchableScope: 'local' | 'global';
};
MetaDetailedOnly: {
features?: {