Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: このユーザーのノートを検索, クエリに基づく検索の初期値 & ノート検索のUI改善 #14128

Merged
merged 28 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2ff5ed6
refactor(frontend): noteSearchAvailableをaccountsに移動
tai-cha Jul 4, 2024
678c139
feat: searchページでのクエリの受取りとtypeによる表示タブの変更
tai-cha Jul 4, 2024
aa37858
user検索でsearchの親から受け取った値を基に入力値を初期化
tai-cha Jul 4, 2024
9ccf32a
feat(frontend): ノート検索で親(search)からの情報を基にユーザー情報を取得
tai-cha Jul 4, 2024
b4f2184
feat(frontend): ユーザーのノートを検索するページに遷移するボタン
tai-cha Jul 4, 2024
76145e4
feat(frontend): ノート検索にホスト名指定のオプション追加
tai-cha Jul 4, 2024
25b2d4c
style: ただ照会部分を囲っただけ(可読性確保のために)
tai-cha Jul 4, 2024
77bdca1
refactor: remove unneed import
tai-cha Jul 4, 2024
72c4658
Update CHANGELOG
tai-cha Jul 4, 2024
83cc950
Fix: ノート検索の初期値が常にホスト指定になってしまう
tai-cha Jul 4, 2024
0e9d275
notesSearchAvailableをaccountに持たせるのをやめる
tai-cha Jul 4, 2024
e1b2a55
SDPX-Licence-Identifier
tai-cha Jul 4, 2024
bc5fb65
Fix: Vitest fails due to instance.policies being undefined
tai-cha Jul 4, 2024
9e0bda5
Add Storybook for search
tai-cha Jul 4, 2024
f2859e2
Fix(storybook): ノート検索が利用できないと出てしまう問題
tai-cha Jul 4, 2024
94c1a5c
storybookでユーザー選択ができないのを修正
tai-cha Jul 4, 2024
9442282
Merge branch 'develop' into feat/serach-this-users-note
tai-cha Jul 4, 2024
4a32a68
Merge branch 'develop' of https://github.com/misskey-dev/misskey into…
tai-cha Jul 10, 2024
3aa7252
feat: ノート検索で自分を選択可能に
tai-cha Jul 10, 2024
be69c3a
Merge branch 'develop' into feat/serach-this-users-note
tai-cha Jul 10, 2024
568e34a
feat(background): api/metaで検索可能なノートのスコープを参照できるように
tai-cha Jul 17, 2024
759920c
globalのノートが検索不可能な場合、検索オプションを表示しないように
tai-cha Jul 17, 2024
5886d06
Merge branch 'develop' of https://github.com/misskey-dev/misskey into…
tai-cha Jul 17, 2024
f681983
Update CHANGELOG.md
tai-cha Jul 17, 2024
578b784
config.meilisearch.scopeがstring[]を取ることがあるので修正
tai-cha Jul 17, 2024
1a17579
meilisearchを利用かつscopeがlocalの場合、リモートユーザーのメニューで「このユーザーのノートを検索」を出さないように
tai-cha Jul 17, 2024
4e769e5
hostが空文字の時の挙動を修正
tai-cha Jul 17, 2024
7ef3bc9
ローカルのみしかノートがインデックスされていない場合、リモートユーザーも選択できなくした
tai-cha Jul 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題

### Client
- Feat: ユーザーページから「このユーザーのノートを検索」できるように (#14128)
- Feat: 検索ページはクエリを受け付けるようになりました (#14128)
- Enhance: 検索ページのUI改善 (#14128)
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
Expand Down
12 changes: 12 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,10 @@ export interface Locale extends ILocale {
* ユーザーを検索
*/
"searchUser": string;
/**
* ユーザーのノートを検索
*/
"searchThisUsersNotes": string;
/**
* 返信
*/
Expand Down Expand Up @@ -776,6 +780,10 @@ export interface Locale extends ILocale {
* ホスト
*/
"host": string;
/**
* 自分を選択
*/
"selectSelf": string;
/**
* ユーザーを選択
*/
Expand Down Expand Up @@ -4460,6 +4468,10 @@ export interface Locale extends ILocale {
* ユーザー指定
*/
"specifyUser": string;
/**
* ホスト指定
*/
"specifyHost": string;
/**
* プレビューできません
*/
Expand Down
3 changes: 3 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ copyFileId: "ファイルIDをコピー"
copyFolderId: "フォルダーIDをコピー"
copyProfileUrl: "プロフィールURLをコピー"
searchUser: "ユーザーを検索"
searchThisUsersNotes: "ユーザーのノートを検索"
reply: "返信"
loadMore: "もっと見る"
showMore: "もっと見る"
Expand Down Expand Up @@ -190,6 +191,7 @@ followConfirm: "{name}をフォローしますか?"
proxyAccount: "プロキシアカウント"
proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。"
host: "ホスト"
selectSelf: "自分を選択"
selectUser: "ユーザーを選択"
recipient: "宛先"
annotation: "注釈"
Expand Down Expand Up @@ -1111,6 +1113,7 @@ preventAiLearning: "生成AIによる学習を拒否"
preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したノートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。"
options: "オプション"
specifyUser: "ユーザー指定"
specifyHost: "ホスト指定"
failedToPreviewUrl: "プレビューできません"
update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール"
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/core/entities/MetaEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export class MetaEntityService {

mediaProxy: this.config.mediaProxy,
enableUrlPreview: instance.urlPreviewEnabled,
noteSearchableScope: this.config.meilisearch == null || this.config.meilisearch.scope === 'global' ? 'global' : 'local',
};

return packed;
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/models/json-schema/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/.storybook/fakes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/.storybook/generate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions packages/frontend/src/components/MkRadios.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Comment on lines +32 to +34
Copy link
Contributor Author

@tai-cha tai-cha Jul 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これ書いたら実環境ではうまく動くけどStorybookで空のMkRadioが表示されるようになっちゃった

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storybookの挙動わからない、tasukete

return () => h('div', {
class: 'novjtcto',
}, [
Expand All @@ -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)),
Expand Down
147 changes: 126 additions & 21 deletions packages/frontend/src/pages/search.note.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>

<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>
<template v-if="user" #suffix>@{{ user.username }}{{ user.host ? `@${user.host}` : "" }}</template>

<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>
kakkokari-gtyih marked this conversation as resolved.
Show resolved Hide resolved
<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>
Expand All @@ -42,38 +51,95 @@ 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 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, host: props.host }).then(_user => {
user.value = _user;
});
}

function selectUser() {
os.selectUser({ includeSelf: true }).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,
Expand All @@ -91,18 +157,57 @@ async function search() {

return;
}
//#endregion

notePagination.value = {
endpoint: 'notes/search',
limit: 10,
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>
Loading
Loading