Skip to content

Commit 863046b

Browse files
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>
1 parent 2b016d6 commit 863046b

8 files changed

Lines changed: 188 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### General
44
- Feat: ジョブキュー管理画面からキューの一時停止/再開ができるように
55
- Feat: アンテナのタイムラインから個別のノートを削除できるように
6+
- Feat: ノート検索で投稿日時の期間を条件に加えられるように(#16035)
67

78
### Client
89
- Fix: URLプレビューのプレイヤーをウィンドウで開いたとき、プレイヤーが読み込まれるまでの間 `Invalid URL` と表示される問題を修正

‎locales/ja-JP.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3358,6 +3358,8 @@ _search:
33583358
pleaseEnterServerHost: "サーバーのホストを入力してください"
33593359
pleaseSelectUser: "ユーザーを選択してください"
33603360
serverHostPlaceholder: "例: misskey.example.com"
3361+
postFrom: "投稿日時from"
3362+
postTo: "投稿日時to"
33613363

33623364
_serverSetupWizard:
33633365
installCompleted: "Misskeyのインストールが完了しました!"

‎packages/backend/src/core/SearchService.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export type SearchOpts = {
3838
userId?: MiNote['userId'] | null;
3939
channelId?: MiNote['channelId'] | null;
4040
host?: string | null;
41+
rangeStartAt?: number | null;
42+
rangeEndAt?: number | null;
4143
};
4244

4345
export type SearchPagination = {
@@ -232,6 +234,16 @@ export class SearchService {
232234
}
233235
}
234236

237+
if (opts.rangeStartAt != null) {
238+
const date = this.idService.gen(opts.rangeStartAt - 1);
239+
query.andWhere('note.id > :rangeStartAt', { rangeStartAt: date });
240+
}
241+
242+
if (opts.rangeEndAt != null) {
243+
const date = this.idService.gen(opts.rangeEndAt + 1);
244+
query.andWhere('note.id < :rangeEndAt', { rangeEndAt: date });
245+
}
246+
235247
this.queryService.generateVisibilityQuery(query, me);
236248
this.queryService.generateBaseNoteFilteringQuery(query, me);
237249

@@ -258,11 +270,21 @@ export class SearchService {
258270
k: 'createdAt',
259271
v: this.idService.parse(pagination.untilId).date.getTime(),
260272
});
273+
if (opts.rangeEndAt) filter.qs.push({
274+
op: '<=',
275+
k: 'createdAt',
276+
v: opts.rangeEndAt,
277+
});
261278
if (pagination.sinceId) filter.qs.push({
262279
op: '>',
263280
k: 'createdAt',
264281
v: this.idService.parse(pagination.sinceId).date.getTime(),
265282
});
283+
if (opts.rangeStartAt) filter.qs.push({
284+
op: '>=',
285+
k: 'createdAt',
286+
v: opts.rangeStartAt,
287+
});
266288
if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId });
267289
if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId });
268290
if (opts.host) {

‎packages/backend/src/server/api/endpoints/notes/search.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export const paramDef = {
3939
type: 'object',
4040
properties: {
4141
query: { type: 'string' },
42+
rangeStartAt: { type: 'integer', nullable: true },
43+
rangeEndAt: { type: 'integer', nullable: true },
4244
sinceId: { type: 'string', format: 'misskey:id' },
4345
untilId: { type: 'string', format: 'misskey:id' },
4446
sinceDate: { type: 'integer' },
@@ -78,6 +80,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
7880
userId: ps.userId,
7981
channelId: ps.channelId,
8082
host: ps.host,
83+
rangeStartAt: ps.rangeStartAt,
84+
rangeEndAt: ps.rangeEndAt,
8185
}, {
8286
untilId: untilId,
8387
sinceId: sinceId,

‎packages/backend/test/unit/SearchService.ts‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,131 @@ describe('SearchService', () => {
284284
expect(remoteResult.map(note => note.id)).toEqual([remoteNote.id]);
285285
});
286286

287+
describe('date range', () => {
288+
test('filters notes after rangeStartAt', async () => {
289+
const ctx = getCtx();
290+
const me = await createUser(ctx, { username: 'me', usernameLower: 'me', host: null });
291+
const author = await createUser(ctx, { username: 'author', usernameLower: 'author', host: null });
292+
293+
const t1 = Date.now() - 3000;
294+
const t2 = Date.now() - 2000;
295+
const t3 = Date.now() - 1000;
296+
297+
await createNote(ctx, author, { text: 'hello old' }, t1);
298+
const note2 = await createNote(ctx, author, { text: 'hello middle' }, t2);
299+
const note3 = await createNote(ctx, author, { text: 'hello new' }, t3);
300+
301+
const result = await ctx.service.searchNote('hello', me, { rangeStartAt: t2 }, { limit: 10 });
302+
expect(result.map(note => note.id)).toEqual([note3.id, note2.id]);
303+
});
304+
305+
test('filters notes before rangeEndAt', async () => {
306+
const ctx = getCtx();
307+
const me = await createUser(ctx, { username: 'me', usernameLower: 'me', host: null });
308+
const author = await createUser(ctx, { username: 'author', usernameLower: 'author', host: null });
309+
310+
const t1 = Date.now() - 3000;
311+
const t2 = Date.now() - 2000;
312+
const t3 = Date.now() - 1000;
313+
314+
const note1 = await createNote(ctx, author, { text: 'hello old' }, t1);
315+
const note2 = await createNote(ctx, author, { text: 'hello middle' }, t2);
316+
await createNote(ctx, author, { text: 'hello new' }, t3);
317+
318+
const result = await ctx.service.searchNote('hello', me, { rangeEndAt: t2 }, { limit: 10 });
319+
expect(result.map(note => note.id)).toEqual([note2.id, note1.id]);
320+
});
321+
322+
test('filters notes between rangeStartAt and rangeEndAt', async () => {
323+
const ctx = getCtx();
324+
const me = await createUser(ctx, { username: 'me', usernameLower: 'me', host: null });
325+
const author = await createUser(ctx, { username: 'author', usernameLower: 'author', host: null });
326+
327+
const t1 = Date.now() - 4000;
328+
const t2 = Date.now() - 3000;
329+
const t3 = Date.now() - 2000;
330+
const t4 = Date.now() - 1000;
331+
332+
await createNote(ctx, author, { text: 'hello old' }, t1);
333+
const note2 = await createNote(ctx, author, { text: 'hello middle' }, t2);
334+
const note3 = await createNote(ctx, author, { text: 'hello newer' }, t3);
335+
await createNote(ctx, author, { text: 'hello new' }, t4);
336+
337+
const result = await ctx.service.searchNote('hello', me, { rangeStartAt: t2, rangeEndAt: t3 }, { limit: 10 });
338+
expect(result.map(note => note.id)).toEqual([note3.id, note2.id]);
339+
});
340+
341+
test('keeps pagination within date range when sinceId and untilId are outside range', async () => {
342+
const ctx = getCtx();
343+
const me = await createUser(ctx, { username: 'me', usernameLower: 'me', host: null });
344+
const author = await createUser(ctx, { username: 'author', usernameLower: 'author', host: null });
345+
346+
const t1 = Date.now() - 8000;
347+
const t2 = Date.now() - 7000;
348+
const t3 = Date.now() - 6000;
349+
const t4 = Date.now() - 5000;
350+
const t5 = Date.now() - 4000;
351+
const t6 = Date.now() - 3000;
352+
const t7 = Date.now() - 2000;
353+
const t8 = Date.now() - 1000;
354+
355+
await createNote(ctx, author, { text: 'hello outside oldest' }, t1);
356+
const sinceCursor = await createNote(ctx, author, { text: 'hello since cursor before range' }, t2);
357+
const beforeRange = await createNote(ctx, author, { text: 'hello before range but after since' }, t3);
358+
const note4 = await createNote(ctx, author, { text: 'hello range old' }, t4);
359+
const note5 = await createNote(ctx, author, { text: 'hello range middle' }, t5);
360+
const note6 = await createNote(ctx, author, { text: 'hello range new' }, t6);
361+
const afterRange = await createNote(ctx, author, { text: 'hello after range but before until' }, t7);
362+
const untilCursor = await createNote(ctx, author, { text: 'hello until cursor after range' }, t8);
363+
364+
const result = await ctx.service.searchNote(
365+
'hello',
366+
me,
367+
{ rangeStartAt: t4, rangeEndAt: t6 },
368+
{ limit: 10, sinceId: sinceCursor.id, untilId: untilCursor.id },
369+
);
370+
const resultIds = result.map(note => note.id);
371+
expect(resultIds).toEqual([note6.id, note5.id, note4.id]);
372+
expect(resultIds).not.toContain(beforeRange.id);
373+
expect(resultIds).not.toContain(afterRange.id);
374+
});
375+
376+
test('uses sinceId and untilId as pagination boundaries inside date range', async () => {
377+
const ctx = getCtx();
378+
const me = await createUser(ctx, { username: 'me', usernameLower: 'me', host: null });
379+
const author = await createUser(ctx, { username: 'author', usernameLower: 'author', host: null });
380+
381+
const t1 = Date.now() - 8000;
382+
const t2 = Date.now() - 7000;
383+
const t3 = Date.now() - 6000;
384+
const t4 = Date.now() - 5000;
385+
const t5 = Date.now() - 4000;
386+
const t6 = Date.now() - 3000;
387+
const t7 = Date.now() - 2000;
388+
const t8 = Date.now() - 1000;
389+
390+
await createNote(ctx, author, { text: 'hello before range' }, t1);
391+
const rangeOldest = await createNote(ctx, author, { text: 'hello range oldest' }, t2);
392+
const sinceCursor = await createNote(ctx, author, { text: 'hello since cursor in range' }, t3);
393+
const note4 = await createNote(ctx, author, { text: 'hello page old' }, t4);
394+
const note5 = await createNote(ctx, author, { text: 'hello page new' }, t5);
395+
const untilCursor = await createNote(ctx, author, { text: 'hello until cursor in range' }, t6);
396+
const rangeNewest = await createNote(ctx, author, { text: 'hello range newest' }, t7);
397+
await createNote(ctx, author, { text: 'hello after range' }, t8);
398+
399+
const result = await ctx.service.searchNote(
400+
'hello',
401+
me,
402+
{ rangeStartAt: t2, rangeEndAt: t7 },
403+
{ limit: 10, sinceId: sinceCursor.id, untilId: untilCursor.id },
404+
);
405+
const resultIds = result.map(note => note.id);
406+
expect(resultIds).toEqual([note5.id, note4.id]);
407+
expect(resultIds).not.toContain(rangeOldest.id);
408+
expect(resultIds).not.toContain(rangeNewest.id);
409+
});
410+
});
411+
287412
describe('muting and blocking', () => {
288413
test('filters out muted users', async () => {
289414
const ctx = getCtx();

‎packages/frontend/src/pages/search.note.vue‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ SPDX-License-Identifier: AGPL-3.0-only
1919
<template #header>{{ i18n.ts.options }}</template>
2020

2121
<div class="_gaps_m">
22+
<div style="display: flex; gap: 8px;">
23+
<MkInput v-model="rangeStartAt" type="datetime-local">
24+
<template #label>{{ i18n.ts._search.postFrom }}</template>
25+
</MkInput>
26+
<MkInput v-model="rangeEndAt" type="datetime-local">
27+
<template #label>{{ i18n.ts._search.postTo }}</template>
28+
</MkInput>
29+
</div>
30+
2231
<MkRadios
2332
v-model="searchScope"
2433
:options="searchScopeDef"
@@ -147,6 +156,8 @@ const paginator = shallowRef<Paginator<'notes/search'> | null>(null);
147156
148157
const searchQuery = ref(toRef(props, 'query').value);
149158
const hostInput = ref(toRef(props, 'host').value);
159+
const rangeStartAt = ref<string | null>(null);
160+
const rangeEndAt = ref<string | null>(null);
150161
151162
const user = shallowRef<Misskey.entities.UserDetailed | null>(null);
152163
@@ -205,13 +216,22 @@ type SearchParams = {
205216
readonly query: string;
206217
readonly host?: string;
207218
readonly userId?: string;
219+
readonly rangeStartAt?: number | null;
220+
readonly rangeEndAt?: number | null;
208221
};
209222
210223
const fixHostIfLocal = (target: string | null | undefined) => {
211224
if (!target || target === localHost) return '.';
212225
return target;
213226
};
214227
228+
const searchRange = () => {
229+
return {
230+
rangeStartAt: rangeStartAt.value ? new Date(rangeStartAt.value).getTime() : null,
231+
rangeEndAt: rangeEndAt.value ? new Date(rangeEndAt.value).getTime() : null,
232+
};
233+
};
234+
215235
const searchParams = computed<SearchParams | null>(() => {
216236
const trimmedQuery = searchQuery.value.trim();
217237
if (!trimmedQuery) return null;
@@ -222,6 +242,7 @@ const searchParams = computed<SearchParams | null>(() => {
222242
query: trimmedQuery,
223243
host: fixHostIfLocal(user.value.host),
224244
userId: user.value.id,
245+
...searchRange(),
225246
};
226247
}
227248
@@ -236,18 +257,21 @@ const searchParams = computed<SearchParams | null>(() => {
236257
return {
237258
query: trimmedQuery,
238259
host: fixHostIfLocal(trimmedHost),
260+
...searchRange(),
239261
};
240262
}
241263
242264
if (instance.federation === 'none' || searchScope.value === 'local') {
243265
return {
244266
query: trimmedQuery,
245267
host: '.',
268+
...searchRange(),
246269
};
247270
}
248271
249272
return {
250273
query: trimmedQuery,
274+
...searchRange(),
251275
};
252276
});
253277

‎packages/i18n/src/autogen/locale.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12553,6 +12553,14 @@ export interface Locale extends ILocale {
1255312553
* 例: misskey.example.com
1255412554
*/
1255512555
"serverHostPlaceholder": string;
12556+
/**
12557+
* 投稿日時from
12558+
*/
12559+
"postFrom": string;
12560+
/**
12561+
* 投稿日時to
12562+
*/
12563+
"postTo": string;
1255612564
};
1255712565
"_serverSetupWizard": {
1255812566
/**

‎packages/misskey-js/src/autogen/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31088,6 +31088,8 @@ export interface operations {
3108831088
content: {
3108931089
'application/json': {
3109031090
query: string;
31091+
rangeStartAt?: number | null;
31092+
rangeEndAt?: number | null;
3109131093
/** Format: misskey:id */
3109231094
sinceId?: string;
3109331095
/** Format: misskey:id */

0 commit comments

Comments
 (0)