検索からハッシュタグのページが開けるように、users/searchに@から始まる文字列が与えられた際の処理を修正 等 (#13858)

* enhance(frontend): 検索からハッシュタグのページを開けるように

* fix(frontend): 照会で入力が`#`のみの場合は`/tags/`に遷移しないように

* docs(changelog): update changelog

* enhance(frontend): ユーザー検索からもハッシュタグのページを開けるように

* docs(changelog): update changelog

* enhance(frontend): 検索範囲等が指定されている時は照会/ハッシュタグページを開かないように

* enhance(frontend): 検索内容に空白が含まれている場合は照会/ハッシュタグページを開かないように

* docs(changelog): update changelog

* Revert "enhance(frontend): 検索範囲等が指定されている時は照会/ハッシュタグページを開かないように"

This reverts commit f84eecea964b90e9b115eac19ed6f19a603a6bbc.

* enhance(frontend): 検索から照会/ハッシュタグページを開くかどうか確認するように

* docs(changelog): update changelog

* chore: fix lint

* docs(changelog): update changelog insertion position

* enhance(frontend): 検索から`@user@host`の形式で照会出来るように

* fix(frontend): 照会で入力が`@`のみの場合に`/@`に遷移しないように

* fix(backend): `users/search`において`@`から始まるqueryに対する処理が正しくなかった問題を修正

* docs(changelog): update changelog

* chore(backend): fix lint error

* fix(backend): more improvements for users/search when query startswith `@`

* chore: unify common conditions

* docs(changelog): refine changelog

* chore(backend): fix lint error

* MkInputをpreventに対応させ、enterの意図せぬ伝搬を防ぐ

* chore(frontend/search.user): use .prevent to prevent the propagation of enter instead of setTimeout

---------

Co-authored-by: samunohito <46447427+samunohito@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Co-authored-by: taichanne30 <dev@taichan.site>
This commit is contained in:
Sayamame-beans 2024-07-30 19:18:43 +09:00 committed by GitHub
parent 2307849c9a
commit d0b7c74fd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 159 additions and 103 deletions

View File

@ -30,6 +30,10 @@
- Enhance: AiScriptを0.19.0にアップデート - Enhance: AiScriptを0.19.0にアップデート
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`) - Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
- Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように - Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように
- Enhance: 検索(ノート/ユーザー)で `#` から始まる文字列を入力すると、そのハッシュタグのノート/ユーザー一覧ページが表示できるように
- Enhance: 検索(ノート/ユーザー)において、入力に空白が含まれている場合は照会を行わないように
- Enhance: 検索(ノート/ユーザー)において、照会を行うかどうか、ハッシュタグのノート/ユーザー一覧ページを表示するかどうかの確認ダイアログを出すように
- Enhance: 検索(ノート/ユーザー)で `@` から始まる文字列(`@user@host`など)を入力すると、そのユーザーを照会できるように
- Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように - Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように
(Cherry-picked from https://github.com/nafu-at/misskey/commit/b89c2af6945c6a9f9f10e83f54d2bcf0f240b0b4, https://github.com/nafu-at/misskey/commit/8a7d710c6acb83f50c83f050bd1423c764d60a99) (Cherry-picked from https://github.com/nafu-at/misskey/commit/b89c2af6945c6a9f9f10e83f54d2bcf0f240b0b4, https://github.com/nafu-at/misskey/commit/8a7d710c6acb83f50c83f050bd1423c764d60a99)
- Enhance: デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように - Enhance: デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように
@ -57,6 +61,8 @@
- Fix: ダイレクト投稿の"削除して編集"において、宛先が保持されていなかった問題を修正 - Fix: ダイレクト投稿の"削除して編集"において、宛先が保持されていなかった問題を修正
- Fix: 投稿フォームへのURL貼り付けによる引用が下書きに保存されていなかった問題を修正 - Fix: 投稿フォームへのURL貼り付けによる引用が下書きに保存されていなかった問題を修正
- Fix: "削除して編集"や下書きにおいて、リアクションの受け入れ設定が保持/保存されていなかった問題を修正 - Fix: "削除して編集"や下書きにおいて、リアクションの受け入れ設定が保持/保存されていなかった問題を修正
- Fix: 照会に `#` から始まる文字列を入力してそのハッシュタグのページを表示する際、入力が `#` のみの場合に「指定されたURLに該当するページはありませんでした。」が表示されてしまう問題を修正
- Fix: 照会に `@` から始まる文字列を入力してユーザーを照会する際、入力が `@` のみの場合に「問題が発生しました」が表示されてしまう問題を修正
- Fix: 投稿フォームにートのURLを貼り付けて"引用として添付"した場合、投稿文を空にすることによるRenote化が出来なかった問題を修正 - Fix: 投稿フォームにートのURLを貼り付けて"引用として添付"した場合、投稿文を空にすることによるRenote化が出来なかった問題を修正
### Server ### Server
@ -96,6 +102,8 @@
- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正 - Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正
(Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1) (Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1)
- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251 - Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251
- Fix: `users/search`において `@` から始まる文字列が与えられた際の処理が正しくなかった問題を修正
- 名前や自己紹介に `@` から始まる文言が含まれるユーザーも検索できるようになります
- Fix: 一部のMisskey以外のソフトウェアからファイルを受け取れない問題 - Fix: 一部のMisskey以外のソフトウェアからファイルを受け取れない問題
(Cherry-picked from https://github.com/Secineralyr/misskey.dream/pull/73/commits/652eaff1e8aa00b890d71d2e1e52c263c1e67c76) (Cherry-picked from https://github.com/Secineralyr/misskey.dream/pull/73/commits/652eaff1e8aa00b890d71d2e1e52c263c1e67c76)
- NOTE: `drive_file`の`url`, `uri`, `src`の上限が512から1024に変更されます - NOTE: `drive_file`の`url`, `uri`, `src`の上限が512から1024に変更されます

8
locales/index.d.ts vendored
View File

@ -4500,6 +4500,14 @@ export interface Locale extends ILocale {
* *
*/ */
"specifyUser": string; "specifyUser": string;
/**
*
*/
"lookupConfirm": string;
/**
*
*/
"openTagPageConfirm": string;
/** /**
* *
*/ */

View File

@ -1121,6 +1121,8 @@ preventAiLearning: "生成AIによる学習を拒否"
preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。" preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。"
options: "オプション" options: "オプション"
specifyUser: "ユーザー指定" specifyUser: "ユーザー指定"
lookupConfirm: "照会しますか?"
openTagPageConfirm: "ハッシュタグのページを開きますか?"
specifyHost: "ホスト指定" specifyHost: "ホスト指定"
failedToPreviewUrl: "プレビューできません" failedToPreviewUrl: "プレビューできません"
update: "更新" update: "更新"

View File

@ -57,88 +57,66 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日 const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
ps.query = ps.query.trim(); ps.query = ps.query.trim();
const isUsername = ps.query.startsWith('@'); const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1;
let users: MiUser[] = []; let users: MiUser[] = [];
if (isUsername) { const nameQuery = this.usersRepository.createQueryBuilder('user')
const usernameQuery = this.usersRepository.createQueryBuilder('user') .where(new Brackets(qb => {
.where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' }) qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
if (ps.origin === 'local') { if (isUsername) {
usernameQuery.andWhere('user.host IS NULL'); qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' });
} else if (ps.origin === 'remote') { } else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username
usernameQuery.andWhere('user.host IS NOT NULL'); qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
}
users = await usernameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
} else {
const nameQuery = this.usersRepository.createQueryBuilder('user')
.where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
// Also search username if it qualifies as username
if (this.userEntityService.validateLocalUsername(ps.query)) {
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
}
}))
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
if (ps.origin === 'local') {
nameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
nameQuery.andWhere('user.host IS NOT NULL');
}
users = await nameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
if (users.length < ps.limit) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');
} else if (ps.origin === 'remote') {
profQuery.andWhere('prof.userHost IS NOT NULL');
} }
}))
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
const query = this.usersRepository.createQueryBuilder('user') if (ps.origin === 'local') {
.where(`user.id IN (${ profQuery.getQuery() })`) nameQuery.andWhere('user.host IS NULL');
.andWhere(new Brackets(qb => { } else if (ps.origin === 'remote') {
qb nameQuery.andWhere('user.host IS NOT NULL');
.where('user.updatedAt IS NULL') }
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE')
.setParameters(profQuery.getParameters());
users = users.concat(await query users = await nameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST') .orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit) .limit(ps.limit)
.offset(ps.offset) .offset(ps.offset)
.getMany(), .getMany();
);
if (users.length < ps.limit) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');
} else if (ps.origin === 'remote') {
profQuery.andWhere('prof.userHost IS NOT NULL');
} }
const query = this.usersRepository.createQueryBuilder('user')
.where(`user.id IN (${ profQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE')
.setParameters(profQuery.getParameters());
users = users.concat(await query
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany(),
);
} }
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' }); return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });

View File

@ -79,7 +79,7 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'change', _ev: KeyboardEvent): void; (ev: 'change', _ev: KeyboardEvent): void;
(ev: 'keydown', _ev: KeyboardEvent): void; (ev: 'keydown', _ev: KeyboardEvent): void;
(ev: 'enter'): void; (ev: 'enter', _ev: KeyboardEvent): void;
(ev: 'update:modelValue', value: string | number): void; (ev: 'update:modelValue', value: string | number): void;
}>(); }>();
@ -111,7 +111,7 @@ const onKeydown = (ev: KeyboardEvent) => {
emit('keydown', ev); emit('keydown', ev);
if (ev.code === 'Enter') { if (ev.code === 'Enter') {
emit('enter'); emit('enter', ev);
} }
}; };

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div class="_gaps"> <div class="_gaps">
<div class="_gaps"> <div class="_gaps">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search"> <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search">
<template #prefix><i class="ti ti-search"></i></template> <template #prefix><i class="ti ti-search"></i></template>
</MkInput> </MkInput>
<MkFoldableSection :expanded="true"> <MkFoldableSection :expanded="true">
@ -143,25 +143,55 @@ async function search() {
if (query == null || query === '') return; if (query == null || query === '') return;
//#region AP lookup //#region AP lookup
if (query.startsWith('https://')) { if (query.startsWith('https://') && !query.includes(' ')) {
const promise = misskeyApi('ap/show', { const confirm = await os.confirm({
uri: query, type: 'info',
text: i18n.ts.lookupConfirm,
}); });
if (!confirm.canceled) {
const promise = misskeyApi('ap/show', {
uri: query,
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
const res = await promise; const res = await promise;
if (res.type === 'User') { if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`); router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === 'Note') { } else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`); router.push(`/notes/${res.object.id}`);
}
return;
} }
return;
} }
//#endregion //#endregion
if (query.length > 1 && !query.includes(' ')) {
if (query.startsWith('@')) {
const confirm = await os.confirm({
type: 'info',
text: i18n.ts.lookupConfirm,
});
if (!confirm.canceled) {
router.push(`/${query}`);
return;
}
}
if (query.startsWith('#')) {
const confirm = await os.confirm({
type: 'info',
text: i18n.ts.openTagPageConfirm,
});
if (!confirm.canceled) {
router.push(`/tags/${encodeURIComponent(query.substring(1))}`);
return;
}
}
}
notePagination.value = { notePagination.value = {
endpoint: 'notes/search', endpoint: 'notes/search',
limit: 10, limit: 10,

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div class="_gaps"> <div class="_gaps">
<div class="_gaps"> <div class="_gaps">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search"> <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search">
<template #prefix><i class="ti ti-search"></i></template> <template #prefix><i class="ti ti-search"></i></template>
</MkInput> </MkInput>
<MkRadios v-model="searchOrigin" @update:modelValue="search()"> <MkRadios v-model="searchOrigin" @update:modelValue="search()">
@ -39,8 +39,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
query?: string, query?: string,
origin?: Endpoints['users/search']['req']['origin'], origin?: Endpoints['users/search']['req']['origin'],
}>(), { }>(), {
query: '', query: '',
origin: 'combined', origin: 'combined',
@ -59,25 +59,55 @@ async function search() {
if (query == null || query === '') return; if (query == null || query === '') return;
//#region AP lookup //#region AP lookup
if (query.startsWith('https://')) { if (query.startsWith('https://') && !query.includes(' ')) {
const promise = misskeyApi('ap/show', { const confirm = await os.confirm({
uri: query, type: 'info',
text: i18n.ts.lookupConfirm,
}); });
if (!confirm.canceled) {
const promise = misskeyApi('ap/show', {
uri: query,
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
const res = await promise; const res = await promise;
if (res.type === 'User') { if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`); router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === 'Note') { } else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`); router.push(`/notes/${res.object.id}`);
}
return;
} }
return;
} }
//#endregion //#endregion
if (query.length > 1 && !query.includes(' ')) {
if (query.startsWith('@')) {
const confirm = await os.confirm({
type: 'info',
text: i18n.ts.lookupConfirm,
});
if (!confirm.canceled) {
router.push(`/${query}`);
return;
}
}
if (query.startsWith('#')) {
const confirm = await os.confirm({
type: 'info',
text: i18n.ts.openTagPageConfirm,
});
if (!confirm.canceled) {
router.push(`/user-tags/${encodeURIComponent(query.substring(1))}`);
return;
}
}
}
userPagination.value = { userPagination.value = {
endpoint: 'users/search', endpoint: 'users/search',
limit: 10, limit: 10,

View File

@ -16,7 +16,7 @@ export async function lookup(router?: Router) {
title: i18n.ts.lookup, title: i18n.ts.lookup,
}); });
const query = temp ? temp.trim() : ''; const query = temp ? temp.trim() : '';
if (canceled) return; if (canceled || query.length <= 1) return;
if (query.startsWith('@') && !query.includes(' ')) { if (query.startsWith('@') && !query.includes(' ')) {
_router.push(`/${query}`); _router.push(`/${query}`);