mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-25 17:10:43 +00:00
feat: 投稿日時の範囲を条件に加えてノート検索出来るようにする (#16119)
* feat: 投稿日時の範囲を条件に加えてノート検索出来るようにする * simplify * fix ui * fix CHANGELOG.md * fix * fix * add test --------- Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>
This commit is contained in:
parent
2b016d670f
commit
863046ba8c
8 changed files with 188 additions and 0 deletions
|
|
@ -3,6 +3,7 @@
|
|||
### General
|
||||
- Feat: ジョブキュー管理画面からキューの一時停止/再開ができるように
|
||||
- Feat: アンテナのタイムラインから個別のノートを削除できるように
|
||||
- Feat: ノート検索で投稿日時の期間を条件に加えられるように(#16035)
|
||||
|
||||
### Client
|
||||
- Fix: URLプレビューのプレイヤーをウィンドウで開いたとき、プレイヤーが読み込まれるまでの間 `Invalid URL` と表示される問題を修正
|
||||
|
|
|
|||
|
|
@ -3358,6 +3358,8 @@ _search:
|
|||
pleaseEnterServerHost: "サーバーのホストを入力してください"
|
||||
pleaseSelectUser: "ユーザーを選択してください"
|
||||
serverHostPlaceholder: "例: misskey.example.com"
|
||||
postFrom: "投稿日時from"
|
||||
postTo: "投稿日時to"
|
||||
|
||||
_serverSetupWizard:
|
||||
installCompleted: "Misskeyのインストールが完了しました!"
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ export type SearchOpts = {
|
|||
userId?: MiNote['userId'] | null;
|
||||
channelId?: MiNote['channelId'] | null;
|
||||
host?: string | null;
|
||||
rangeStartAt?: number | null;
|
||||
rangeEndAt?: number | null;
|
||||
};
|
||||
|
||||
export type SearchPagination = {
|
||||
|
|
@ -232,6 +234,16 @@ export class SearchService {
|
|||
}
|
||||
}
|
||||
|
||||
if (opts.rangeStartAt != null) {
|
||||
const date = this.idService.gen(opts.rangeStartAt - 1);
|
||||
query.andWhere('note.id > :rangeStartAt', { rangeStartAt: date });
|
||||
}
|
||||
|
||||
if (opts.rangeEndAt != null) {
|
||||
const date = this.idService.gen(opts.rangeEndAt + 1);
|
||||
query.andWhere('note.id < :rangeEndAt', { rangeEndAt: date });
|
||||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateBaseNoteFilteringQuery(query, me);
|
||||
|
||||
|
|
@ -258,11 +270,21 @@ export class SearchService {
|
|||
k: 'createdAt',
|
||||
v: this.idService.parse(pagination.untilId).date.getTime(),
|
||||
});
|
||||
if (opts.rangeEndAt) filter.qs.push({
|
||||
op: '<=',
|
||||
k: 'createdAt',
|
||||
v: opts.rangeEndAt,
|
||||
});
|
||||
if (pagination.sinceId) filter.qs.push({
|
||||
op: '>',
|
||||
k: 'createdAt',
|
||||
v: this.idService.parse(pagination.sinceId).date.getTime(),
|
||||
});
|
||||
if (opts.rangeStartAt) filter.qs.push({
|
||||
op: '>=',
|
||||
k: 'createdAt',
|
||||
v: opts.rangeStartAt,
|
||||
});
|
||||
if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId });
|
||||
if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId });
|
||||
if (opts.host) {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' },
|
||||
rangeStartAt: { type: 'integer', nullable: true },
|
||||
rangeEndAt: { type: 'integer', nullable: true },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
|
|
@ -78,6 +80,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
userId: ps.userId,
|
||||
channelId: ps.channelId,
|
||||
host: ps.host,
|
||||
rangeStartAt: ps.rangeStartAt,
|
||||
rangeEndAt: ps.rangeEndAt,
|
||||
}, {
|
||||
untilId: untilId,
|
||||
sinceId: sinceId,
|
||||
|
|
|
|||
|
|
@ -284,6 +284,131 @@ describe('SearchService', () => {
|
|||
expect(remoteResult.map(note => note.id)).toEqual([remoteNote.id]);
|
||||
});
|
||||
|
||||
describe('date range', () => {
|
||||
test('filters notes after rangeStartAt', async () => {
|
||||
const ctx = getCtx();
|
||||
const me = await createUser(ctx, { username: 'me', usernameLower: 'me', host: null });
|
||||
const author = await createUser(ctx, { username: 'author', usernameLower: 'author', host: null });
|
||||
|
||||
const t1 = Date.now() - 3000;
|
||||
const t2 = Date.now() - 2000;
|
||||
const t3 = Date.now() - 1000;
|
||||
|
||||
await createNote(ctx, author, { text: 'hello old' }, t1);
|
||||
const note2 = await createNote(ctx, author, { text: 'hello middle' }, t2);
|
||||
const note3 = await createNote(ctx, author, { text: 'hello new' }, t3);
|
||||
|
||||
const result = await ctx.service.searchNote('hello', me, { rangeStartAt: t2 }, { limit: 10 });
|
||||
expect(result.map(note => note.id)).toEqual([note3.id, note2.id]);
|
||||
});
|
||||
|
||||
test('filters notes before rangeEndAt', async () => {
|
||||
const ctx = getCtx();
|
||||
const me = await createUser(ctx, { username: 'me', usernameLower: 'me', host: null });
|
||||
const author = await createUser(ctx, { username: 'author', usernameLower: 'author', host: null });
|
||||
|
||||
const t1 = Date.now() - 3000;
|
||||
const t2 = Date.now() - 2000;
|
||||
const t3 = Date.now() - 1000;
|
||||
|
||||
const note1 = await createNote(ctx, author, { text: 'hello old' }, t1);
|
||||
const note2 = await createNote(ctx, author, { text: 'hello middle' }, t2);
|
||||
await createNote(ctx, author, { text: 'hello new' }, t3);
|
||||
|
||||
const result = await ctx.service.searchNote('hello', me, { rangeEndAt: t2 }, { limit: 10 });
|
||||
expect(result.map(note => note.id)).toEqual([note2.id, note1.id]);
|
||||
});
|
||||
|
||||
test('filters notes between rangeStartAt and rangeEndAt', async () => {
|
||||
const ctx = getCtx();
|
||||
const me = await createUser(ctx, { username: 'me', usernameLower: 'me', host: null });
|
||||
const author = await createUser(ctx, { username: 'author', usernameLower: 'author', host: null });
|
||||
|
||||
const t1 = Date.now() - 4000;
|
||||
const t2 = Date.now() - 3000;
|
||||
const t3 = Date.now() - 2000;
|
||||
const t4 = Date.now() - 1000;
|
||||
|
||||
await createNote(ctx, author, { text: 'hello old' }, t1);
|
||||
const note2 = await createNote(ctx, author, { text: 'hello middle' }, t2);
|
||||
const note3 = await createNote(ctx, author, { text: 'hello newer' }, t3);
|
||||
await createNote(ctx, author, { text: 'hello new' }, t4);
|
||||
|
||||
const result = await ctx.service.searchNote('hello', me, { rangeStartAt: t2, rangeEndAt: t3 }, { limit: 10 });
|
||||
expect(result.map(note => note.id)).toEqual([note3.id, note2.id]);
|
||||
});
|
||||
|
||||
test('keeps pagination within date range when sinceId and untilId are outside range', async () => {
|
||||
const ctx = getCtx();
|
||||
const me = await createUser(ctx, { username: 'me', usernameLower: 'me', host: null });
|
||||
const author = await createUser(ctx, { username: 'author', usernameLower: 'author', host: null });
|
||||
|
||||
const t1 = Date.now() - 8000;
|
||||
const t2 = Date.now() - 7000;
|
||||
const t3 = Date.now() - 6000;
|
||||
const t4 = Date.now() - 5000;
|
||||
const t5 = Date.now() - 4000;
|
||||
const t6 = Date.now() - 3000;
|
||||
const t7 = Date.now() - 2000;
|
||||
const t8 = Date.now() - 1000;
|
||||
|
||||
await createNote(ctx, author, { text: 'hello outside oldest' }, t1);
|
||||
const sinceCursor = await createNote(ctx, author, { text: 'hello since cursor before range' }, t2);
|
||||
const beforeRange = await createNote(ctx, author, { text: 'hello before range but after since' }, t3);
|
||||
const note4 = await createNote(ctx, author, { text: 'hello range old' }, t4);
|
||||
const note5 = await createNote(ctx, author, { text: 'hello range middle' }, t5);
|
||||
const note6 = await createNote(ctx, author, { text: 'hello range new' }, t6);
|
||||
const afterRange = await createNote(ctx, author, { text: 'hello after range but before until' }, t7);
|
||||
const untilCursor = await createNote(ctx, author, { text: 'hello until cursor after range' }, t8);
|
||||
|
||||
const result = await ctx.service.searchNote(
|
||||
'hello',
|
||||
me,
|
||||
{ rangeStartAt: t4, rangeEndAt: t6 },
|
||||
{ limit: 10, sinceId: sinceCursor.id, untilId: untilCursor.id },
|
||||
);
|
||||
const resultIds = result.map(note => note.id);
|
||||
expect(resultIds).toEqual([note6.id, note5.id, note4.id]);
|
||||
expect(resultIds).not.toContain(beforeRange.id);
|
||||
expect(resultIds).not.toContain(afterRange.id);
|
||||
});
|
||||
|
||||
test('uses sinceId and untilId as pagination boundaries inside date range', async () => {
|
||||
const ctx = getCtx();
|
||||
const me = await createUser(ctx, { username: 'me', usernameLower: 'me', host: null });
|
||||
const author = await createUser(ctx, { username: 'author', usernameLower: 'author', host: null });
|
||||
|
||||
const t1 = Date.now() - 8000;
|
||||
const t2 = Date.now() - 7000;
|
||||
const t3 = Date.now() - 6000;
|
||||
const t4 = Date.now() - 5000;
|
||||
const t5 = Date.now() - 4000;
|
||||
const t6 = Date.now() - 3000;
|
||||
const t7 = Date.now() - 2000;
|
||||
const t8 = Date.now() - 1000;
|
||||
|
||||
await createNote(ctx, author, { text: 'hello before range' }, t1);
|
||||
const rangeOldest = await createNote(ctx, author, { text: 'hello range oldest' }, t2);
|
||||
const sinceCursor = await createNote(ctx, author, { text: 'hello since cursor in range' }, t3);
|
||||
const note4 = await createNote(ctx, author, { text: 'hello page old' }, t4);
|
||||
const note5 = await createNote(ctx, author, { text: 'hello page new' }, t5);
|
||||
const untilCursor = await createNote(ctx, author, { text: 'hello until cursor in range' }, t6);
|
||||
const rangeNewest = await createNote(ctx, author, { text: 'hello range newest' }, t7);
|
||||
await createNote(ctx, author, { text: 'hello after range' }, t8);
|
||||
|
||||
const result = await ctx.service.searchNote(
|
||||
'hello',
|
||||
me,
|
||||
{ rangeStartAt: t2, rangeEndAt: t7 },
|
||||
{ limit: 10, sinceId: sinceCursor.id, untilId: untilCursor.id },
|
||||
);
|
||||
const resultIds = result.map(note => note.id);
|
||||
expect(resultIds).toEqual([note5.id, note4.id]);
|
||||
expect(resultIds).not.toContain(rangeOldest.id);
|
||||
expect(resultIds).not.toContain(rangeNewest.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('muting and blocking', () => {
|
||||
test('filters out muted users', async () => {
|
||||
const ctx = getCtx();
|
||||
|
|
|
|||
|
|
@ -19,6 +19,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header>{{ i18n.ts.options }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<MkInput v-model="rangeStartAt" type="datetime-local">
|
||||
<template #label>{{ i18n.ts._search.postFrom }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="rangeEndAt" type="datetime-local">
|
||||
<template #label>{{ i18n.ts._search.postTo }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
|
||||
<MkRadios
|
||||
v-model="searchScope"
|
||||
:options="searchScopeDef"
|
||||
|
|
@ -147,6 +156,8 @@ const paginator = shallowRef<Paginator<'notes/search'> | null>(null);
|
|||
|
||||
const searchQuery = ref(toRef(props, 'query').value);
|
||||
const hostInput = ref(toRef(props, 'host').value);
|
||||
const rangeStartAt = ref<string | null>(null);
|
||||
const rangeEndAt = ref<string | null>(null);
|
||||
|
||||
const user = shallowRef<Misskey.entities.UserDetailed | null>(null);
|
||||
|
||||
|
|
@ -205,6 +216,8 @@ type SearchParams = {
|
|||
readonly query: string;
|
||||
readonly host?: string;
|
||||
readonly userId?: string;
|
||||
readonly rangeStartAt?: number | null;
|
||||
readonly rangeEndAt?: number | null;
|
||||
};
|
||||
|
||||
const fixHostIfLocal = (target: string | null | undefined) => {
|
||||
|
|
@ -212,6 +225,13 @@ const fixHostIfLocal = (target: string | null | undefined) => {
|
|||
return target;
|
||||
};
|
||||
|
||||
const searchRange = () => {
|
||||
return {
|
||||
rangeStartAt: rangeStartAt.value ? new Date(rangeStartAt.value).getTime() : null,
|
||||
rangeEndAt: rangeEndAt.value ? new Date(rangeEndAt.value).getTime() : null,
|
||||
};
|
||||
};
|
||||
|
||||
const searchParams = computed<SearchParams | null>(() => {
|
||||
const trimmedQuery = searchQuery.value.trim();
|
||||
if (!trimmedQuery) return null;
|
||||
|
|
@ -222,6 +242,7 @@ const searchParams = computed<SearchParams | null>(() => {
|
|||
query: trimmedQuery,
|
||||
host: fixHostIfLocal(user.value.host),
|
||||
userId: user.value.id,
|
||||
...searchRange(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -236,6 +257,7 @@ const searchParams = computed<SearchParams | null>(() => {
|
|||
return {
|
||||
query: trimmedQuery,
|
||||
host: fixHostIfLocal(trimmedHost),
|
||||
...searchRange(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -243,11 +265,13 @@ const searchParams = computed<SearchParams | null>(() => {
|
|||
return {
|
||||
query: trimmedQuery,
|
||||
host: '.',
|
||||
...searchRange(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
query: trimmedQuery,
|
||||
...searchRange(),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12553,6 +12553,14 @@ export interface Locale extends ILocale {
|
|||
* 例: misskey.example.com
|
||||
*/
|
||||
"serverHostPlaceholder": string;
|
||||
/**
|
||||
* 投稿日時from
|
||||
*/
|
||||
"postFrom": string;
|
||||
/**
|
||||
* 投稿日時to
|
||||
*/
|
||||
"postTo": string;
|
||||
};
|
||||
"_serverSetupWizard": {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -31088,6 +31088,8 @@ export interface operations {
|
|||
content: {
|
||||
'application/json': {
|
||||
query: string;
|
||||
rangeStartAt?: number | null;
|
||||
rangeEndAt?: number | null;
|
||||
/** Format: misskey:id */
|
||||
sinceId?: string;
|
||||
/** Format: misskey:id */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue