This commit is contained in:
果物リン 2025-09-14 05:54:58 +05:30 committed by GitHub
commit 81d774ad10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 70 additions and 14 deletions

View File

@ -38,6 +38,7 @@ export type SearchOpts = {
userId?: MiNote['userId'] | null; userId?: MiNote['userId'] | null;
channelId?: MiNote['channelId'] | null; channelId?: MiNote['channelId'] | null;
host?: string | null; host?: string | null;
searchFrom? : string | null;
}; };
export type SearchPagination = { export type SearchPagination = {
@ -184,6 +185,7 @@ export class SearchService {
case 'sqlPgroonga': { case 'sqlPgroonga': {
// ほとんど内容に差がないのでsqlLikeとsqlPgroongaを同じ処理にしている. // ほとんど内容に差がないのでsqlLikeとsqlPgroongaを同じ処理にしている.
// 今後の拡張で差が出る用であれば関数を分ける. // 今後の拡張で差が出る用であれば関数を分ける.
return this.searchNoteByLike(q, me, opts, pagination); return this.searchNoteByLike(q, me, opts, pagination);
} }
case 'meilisearch': { case 'meilisearch': {
@ -220,10 +222,27 @@ export class SearchService {
.leftJoinAndSelect('renote.user', 'renoteUser'); .leftJoinAndSelect('renote.user', 'renoteUser');
if (this.config.fulltextSearch?.provider === 'sqlPgroonga') { if (this.config.fulltextSearch?.provider === 'sqlPgroonga') {
query.andWhere('note.text &@~ :q', { q }); // pgroonga
if(opts.searchFrom === 'textWithCw'){
// textWithCwオプション
query.andWhere('(coalesce(note.cw, \'\') || note.text) &@~ :q', { q });
}else {
// 通常検索
query.andWhere('note.text &@~ :q', { q });
}
} else { } else {
query.andWhere('LOWER(note.text) LIKE :q', { q: `%${ sqlLikeEscape(q.toLowerCase()) }%` }); // Postgresql 標準
} if(opts.searchFrom === 'textWithCw'){
// textWithCwオプション
query.andWhere('LOWER((coalesce(note.cw, \'\') || note.text)) LIKE :q', { q: `%${ sqlLikeEscape(q.toLowerCase()) }%` });
}else {
// 通常検索
query.andWhere('LOWER(note.text) LIKE :q', { q: `%${ sqlLikeEscape(q.toLowerCase()) }%` });
}
}
if (opts.host) { if (opts.host) {
if (opts.host === '.') { if (opts.host === '.') {

View File

@ -51,6 +51,7 @@ export const paramDef = {
}, },
userId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
searchFrom: { type: 'string', nullable: true, default: null },
}, },
required: ['query'], required: ['query'],
} as const; } as const;
@ -78,6 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: ps.userId, userId: ps.userId,
channelId: ps.channelId, channelId: ps.channelId,
host: ps.host, host: ps.host,
searchFrom: ps.searchFrom
}, { }, {
untilId: untilId, untilId: untilId,
sinceId: sinceId, sinceId: sinceId,

View File

@ -15,8 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
> >
<template #prefix><i class="ti ti-search"></i></template> <template #prefix><i class="ti ti-search"></i></template>
</MkInput> </MkInput>
<MkFoldableSection expanded> <div >
<template #header>{{ i18n.ts.options }}</template> <div >{{ i18n.ts.options }}</div>
<div class="_gaps_m"> <div class="_gaps_m">
<MkRadios v-model="searchScope"> <MkRadios v-model="searchScope">
@ -26,6 +26,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="user">{{ i18n.ts._search.searchScopeUser }}</option> <option value="user">{{ i18n.ts._search.searchScopeUser }}</option>
</MkRadios> </MkRadios>
<MkRadios v-model="searchFrom">
<option value="text">本文</option>
<option value="textWithCw">本文+CW</option>
</MkRadios>
<div v-if="instance.federation !== 'none' && searchScope === 'server'" :class="$style.subOptionRoot"> <div v-if="instance.federation !== 'none' && searchScope === 'server'" :class="$style.subOptionRoot">
<MkInput <MkInput
v-model="hostInput" v-model="hostInput"
@ -87,31 +92,37 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
</div> </div>
</MkFoldableSection> </div>
<div> <div>
<MkButton <MkButton
large large
primary primary
gradate gradate
rounded rounded
:disabled="searchParams == null" :disabled="
searchParams == null
|| !(componentBlockSearchUntil && now? componentBlockSearchUntil < now : true) // disable
"
style="margin: 0 auto;" style="margin: 0 auto;"
@click="search" @click="search"
> >
{{ i18n.ts.search }} {{ i18n.ts.search }}
<MkTime v-if="componentBlockSearchUntil && now && componentBlockSearchUntil > now" :time="componentBlockSearchUntil" />
</MkButton> </MkButton>
</div> </div>
</div> </div>
<MkFoldableSection v-if="paginator"> <div v-if="paginator">
<template #header>{{ i18n.ts.searchResult }}</template> <div >{{ i18n.ts.searchResult }}</div>
<MkNotesTimeline :key="`searchNotes:${key}`" :paginator="paginator"/> <MkNotesTimeline :key="`searchNotes:${key}`" :paginator="paginator"/>
</MkFoldableSection> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, markRaw, ref, shallowRef, toRef } from 'vue'; import { computed, markRaw, ref, shallowRef, toRef} from 'vue';
import { host as localHost } from '@@/js/config.js'; import { host as localHost } from '@@/js/config.js';
import type * as Misskey from 'misskey-js'; import type * as Misskey from 'misskey-js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
@ -122,7 +133,6 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { apLookup } from '@/utility/lookup.js'; import { apLookup } from '@/utility/lookup.js';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue'; import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
@ -179,15 +189,17 @@ if (fetchedUser != null) {
const searchScope = ref<'all' | 'local' | 'server' | 'user'>((() => { const searchScope = ref<'all' | 'local' | 'server' | 'user'>((() => {
if (user.value != null) return 'user'; if (user.value != null) return 'user';
if (noteSearchableScope === 'local') return 'local';
if (hostInput.value) return 'server'; if (hostInput.value) return 'server';
return 'all'; return 'local';
})()); })());
const searchFrom = ref<'text' | 'textWithCw'>('textWithCw');
type SearchParams = { type SearchParams = {
readonly query: string; readonly query: string;
readonly host?: string; readonly host?: string;
readonly userId?: string; readonly userId?: string;
readonly searchFrom?: string;
}; };
const fixHostIfLocal = (target: string | null | undefined) => { const fixHostIfLocal = (target: string | null | undefined) => {
@ -205,6 +217,7 @@ const searchParams = computed<SearchParams | null>(() => {
query: trimmedQuery, query: trimmedQuery,
host: fixHostIfLocal(user.value.host), host: fixHostIfLocal(user.value.host),
userId: user.value.id, userId: user.value.id,
searchFrom: searchFrom.value,
}; };
} }
@ -219,6 +232,7 @@ const searchParams = computed<SearchParams | null>(() => {
return { return {
query: trimmedQuery, query: trimmedQuery,
host: fixHostIfLocal(trimmedHost), host: fixHostIfLocal(trimmedHost),
searchFrom: searchFrom.value
}; };
} }
@ -226,11 +240,13 @@ const searchParams = computed<SearchParams | null>(() => {
return { return {
query: trimmedQuery, query: trimmedQuery,
host: '.', host: '.',
searchFrom: searchFrom.value
}; };
} }
return { return {
query: trimmedQuery, query: trimmedQuery,
searchFrom: searchFrom.value
}; };
}); });
@ -251,9 +267,28 @@ function removeUser() {
user.value = null; user.value = null;
} }
const localBlockSearchUntil = localStorage.getItem('noteSearchedAt')
const componentBlockSearchUntil = ref(localBlockSearchUntil ? new Date(parseInt(localBlockSearchUntil)): null)
const now = ref<Date | null >(new Date())
async function search() { async function search() {
if (searchParams.value == null) return; if (searchParams.value == null) return;
//
const noteSearchedAt = localStorage.getItem('noteSearchedAt');
const now = Date.now();
if (noteSearchedAt && now < parseInt(noteSearchedAt)) {
return;
}
localStorage.setItem('noteSearchedAt', now.toString());
componentBlockSearchUntil.value = new Date(now + 6 * 1000)
console.log(componentBlockSearchUntil.value)
setTimeout(() => {
localStorage.removeItem('noteSearchedAt')
componentBlockSearchUntil.value = null
}, 6 * 1000)
//#region AP lookup //#region AP lookup
if (searchParams.value.query.startsWith('https://') && !searchParams.value.query.includes(' ')) { if (searchParams.value.query.startsWith('https://') && !searchParams.value.query.includes(' ')) {
const confirm = await os.confirm({ const confirm = await os.confirm({