summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-10-06 18:30:08 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-10-06 18:30:08 +0900
commita5b6e807bb6aaf3212f88b4ec4f96c285a80e390 (patch)
treeb33b8a2810d19eea0fba105c0686526821900bae
parent2023.10.0-beta.4 (diff)
downloadsharkey-a5b6e807bb6aaf3212f88b4ec4f96c285a80e390.tar.gz
sharkey-a5b6e807bb6aaf3212f88b4ec4f96c285a80e390.tar.bz2
sharkey-a5b6e807bb6aaf3212f88b4ec4f96c285a80e390.zip
feat: per user featured notes
-rw-r--r--CHANGELOG.md2
-rw-r--r--packages/backend/src/core/FeaturedService.ts15
-rw-r--r--packages/backend/src/core/NoteCreateService.ts5
-rw-r--r--packages/backend/src/core/ReactionService.ts3
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts4
-rw-r--r--packages/backend/src/server/api/endpoints.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/featured.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/users/featured-notes.ts80
-rw-r--r--packages/frontend/src/pages/channel.vue1
-rw-r--r--packages/frontend/src/pages/explore.featured.vue1
-rw-r--r--packages/frontend/src/pages/user/home.vue7
11 files changed, 116 insertions, 11 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7e8b820ec..f9ccaa9f1e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,10 +21,12 @@
### Changes
- API: users/notes, notes/local-timeline で fileType 指定はできなくなりました
- API: notes/global-timeline は現在常に `[]` を返します
+- API: notes/featured でページネーションは他APIと同様 untilId を使って行うようになりました
### General
- Feat: ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました
- Feat: ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました
+- Feat: ユーザーごとのハイライト
- Enhance: ソフトワードミュートとハードワードミュートは統合されました
- Enhance: モデレーションログ機能の強化
- Enhance: ローカリゼーションの更新
diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts
index e8a08cd866..945c23b0e2 100644
--- a/packages/backend/src/core/FeaturedService.ts
+++ b/packages/backend/src/core/FeaturedService.ts
@@ -5,11 +5,12 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
-import type { MiNote } from '@/models/_.js';
+import type { MiNote, MiUser } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
+const PER_USER_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 7; // 1週間ごと
@Injectable()
export class FeaturedService {
@@ -78,11 +79,16 @@ export class FeaturedService {
}
@bindThis
- public updateInChannelNotesRanking(noteId: MiNote['id'], channelId: MiNote['channelId'], score = 1): Promise<void> {
+ public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise<void> {
return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
}
@bindThis
+ public updatePerUserNotesRanking(userId: MiUser['id'], noteId: MiNote['id'], score = 1): Promise<void> {
+ return this.updateRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, noteId, score);
+ }
+
+ @bindThis
public getGlobalNotesRanking(limit: number): Promise<MiNote['id'][]> {
return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, limit);
}
@@ -91,4 +97,9 @@ export class FeaturedService {
public getInChannelNotesRanking(channelId: MiNote['channelId'], limit: number): Promise<MiNote['id'][]> {
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, limit);
}
+
+ @bindThis
+ public getPerUserNotesRanking(userId: MiUser['id'], limit: number): Promise<MiNote['id'][]> {
+ return this.getRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, limit);
+ }
}
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 32f1af4528..b6fc4b3c49 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -729,9 +729,10 @@ export class NoteCreateService implements OnApplicationShutdown {
// 30%の確率でハイライト用ランキング更新
if (Math.random() < 0.3) {
if (renote.channelId != null) {
- this.featuredService.updateInChannelNotesRanking(renote.id, renote.channelId, 1);
+ this.featuredService.updateInChannelNotesRanking(renote.channelId, renote.id, 5);
} else if (renote.visibility === 'public' && renote.userHost == null) {
- this.featuredService.updateGlobalNotesRanking(renote.id, 1);
+ this.featuredService.updateGlobalNotesRanking(renote.id, 5);
+ this.featuredService.updatePerUserNotesRanking(renote.userId, renote.id, 5);
}
}
}
diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts
index e409495de5..63cf4be322 100644
--- a/packages/backend/src/core/ReactionService.ts
+++ b/packages/backend/src/core/ReactionService.ts
@@ -195,9 +195,10 @@ export class ReactionService {
// 30%の確率でハイライト用ランキング更新
if (Math.random() < 0.3 && note.userId !== user.id) {
if (note.channelId != null) {
- this.featuredService.updateInChannelNotesRanking(note.id, note.channelId, 1);
+ this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1);
} else if (note.visibility === 'public' && note.userHost == null) {
this.featuredService.updateGlobalNotesRanking(note.id, 1);
+ this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1);
}
}
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 3c4adafdbd..f834561456 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -325,6 +325,7 @@ import * as ep___users_followers from './endpoints/users/followers.js';
import * as ep___users_following from './endpoints/users/following.js';
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
+import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
import * as ep___users_lists_create from './endpoints/users/lists/create.js';
import * as ep___users_lists_delete from './endpoints/users/lists/delete.js';
import * as ep___users_lists_list from './endpoints/users/lists/list.js';
@@ -674,6 +675,7 @@ const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep
const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default };
const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default };
const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default };
+const $users_featuredNotes: Provider = { provide: 'ep:users/featured-notes', useClass: ep___users_featuredNotes.default };
const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default };
const $users_lists_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default };
const $users_lists_list: Provider = { provide: 'ep:users/lists/list', useClass: ep___users_lists_list.default };
@@ -1027,6 +1029,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_following,
$users_gallery_posts,
$users_getFrequentlyRepliedUsers,
+ $users_featuredNotes,
$users_lists_create,
$users_lists_delete,
$users_lists_list,
@@ -1371,6 +1374,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_following,
$users_gallery_posts,
$users_getFrequentlyRepliedUsers,
+ $users_featuredNotes,
$users_lists_create,
$users_lists_delete,
$users_lists_list,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 199d910fc4..d12a035afa 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -325,6 +325,7 @@ import * as ep___users_followers from './endpoints/users/followers.js';
import * as ep___users_following from './endpoints/users/following.js';
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
+import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
import * as ep___users_lists_create from './endpoints/users/lists/create.js';
import * as ep___users_lists_delete from './endpoints/users/lists/delete.js';
import * as ep___users_lists_list from './endpoints/users/lists/list.js';
@@ -672,6 +673,7 @@ const eps = [
['users/following', ep___users_following],
['users/gallery/posts', ep___users_gallery_posts],
['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers],
+ ['users/featured-notes', ep___users_featuredNotes],
['users/lists/create', ep___users_lists_create],
['users/lists/delete', ep___users_lists_delete],
['users/lists/list', ep___users_lists_list],
diff --git a/packages/backend/src/server/api/endpoints/notes/featured.ts b/packages/backend/src/server/api/endpoints/notes/featured.ts
index bf4ad1deb6..c456874309 100644
--- a/packages/backend/src/server/api/endpoints/notes/featured.ts
+++ b/packages/backend/src/server/api/endpoints/notes/featured.ts
@@ -32,7 +32,7 @@ export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
- offset: { type: 'integer', default: 0 },
+ untilId: { type: 'string', format: 'misskey:id' },
channelId: { type: 'string', nullable: true, format: 'misskey:id' },
},
required: [],
@@ -69,7 +69,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
noteIds.sort((a, b) => a > b ? -1 : 1);
- noteIds.slice(ps.offset, ps.offset + ps.limit);
+ if (ps.untilId) {
+ noteIds = noteIds.filter(id => id < ps.untilId!);
+ }
+ noteIds = noteIds.slice(0, ps.limit);
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
diff --git a/packages/backend/src/server/api/endpoints/users/featured-notes.ts b/packages/backend/src/server/api/endpoints/users/featured-notes.ts
new file mode 100644
index 0000000000..fdf36a6ae0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/featured-notes.ts
@@ -0,0 +1,80 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import type { NotesRepository } from '@/models/_.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { DI } from '@/di-symbols.js';
+import { FeaturedService } from '@/core/FeaturedService.js';
+
+export const meta = {
+ tags: ['notes'],
+
+ requireCredential: false,
+ allowGet: true,
+ cacheSec: 3600,
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'Note',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ untilId: { type: 'string', format: 'misskey:id' },
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['userId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+
+ private noteEntityService: NoteEntityService,
+ private featuredService: FeaturedService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ let noteIds = await this.featuredService.getPerUserNotesRanking(ps.userId, 50);
+
+ if (noteIds.length === 0) {
+ return [];
+ }
+
+ noteIds.sort((a, b) => a > b ? -1 : 1);
+ if (ps.untilId) {
+ noteIds = noteIds.filter(id => id < ps.untilId!);
+ }
+ noteIds = noteIds.slice(0, ps.limit);
+
+ const query = this.notesRepository.createQueryBuilder('note')
+ .where('note.id IN (:...noteIds)', { noteIds: noteIds })
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
+ .leftJoinAndSelect('note.channel', 'channel');
+
+ const notes = await query.getMany();
+ notes.sort((a, b) => a.id > b.id ? -1 : 1);
+
+ // TODO: ミュート等考慮
+
+ return await this.noteEntityService.packMany(notes, me);
+ });
+ }
+}
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 00e88cbbfb..911f4e95d2 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -102,7 +102,6 @@ let searchKey = $ref('');
const featuredPagination = $computed(() => ({
endpoint: 'notes/featured' as const,
limit: 10,
- offsetMode: true,
params: {
channelId: props.channelId,
},
diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue
index 0558faec16..a36d1b3bda 100644
--- a/packages/frontend/src/pages/explore.featured.vue
+++ b/packages/frontend/src/pages/explore.featured.vue
@@ -22,7 +22,6 @@ import { i18n } from '@/i18n.js';
const paginationForNotes = {
endpoint: 'notes/featured' as const,
limit: 10,
- offsetMode: true,
};
const paginationForPolls = {
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 71eec0aa26..605e9fbb76 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -131,7 +131,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<XFiles :key="user.id" :user="user"/>
<XActivity :key="user.id" :user="user"/>
</template>
- <MkNotes v-if="!disableNotes" :class="$style.tl" :noGap="true" :pagination="pagination"/>
+ <div v-if="!disableNotes">
+ <div style="margin-bottom: 8px;">{{ i18n.ts.featured }}</div>
+ <MkNotes :class="$style.tl" :noGap="true" :pagination="pagination"/>
+ </div>
</div>
</div>
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
@@ -210,7 +213,7 @@ watch($$(moderationNote), async () => {
});
const pagination = {
- endpoint: 'users/notes' as const,
+ endpoint: 'users/featured-notes' as const,
limit: 10,
params: computed(() => ({
userId: props.user.id,