diff --git a/locales/en-US.yml b/locales/en-US.yml index f68369021..104d05cb8 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1247,6 +1247,7 @@ _timelines: local: "Local" social: "Social" global: "Global" + shuffled: "Shuffled" _pages: newPage: "Create a new Page" editPage: "Edit this Page" diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index c5249191e..a0d629439 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -229,6 +229,7 @@ import * as ep___notes_replies from './endpoints/notes/replies.js'; import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js'; import * as ep___notes_search from './endpoints/notes/search.js'; import * as ep___notes_show from './endpoints/notes/show.js'; +import * as ep___notes_shuffled from './endpoints/notes/shuffled.js'; import * as ep___notes_state from './endpoints/notes/state.js'; import * as ep___notes_threadMuting_create from './endpoints/notes/thread-muting/create.js'; import * as ep___notes_threadMuting_delete from './endpoints/notes/thread-muting/delete.js'; @@ -523,6 +524,7 @@ const eps = [ ['notes/search-by-tag', ep___notes_searchByTag], ['notes/search', ep___notes_search], ['notes/show', ep___notes_show], + ['notes/shuffled', ep___notes_shuffled], ['notes/state', ep___notes_state], ['notes/thread-muting/create', ep___notes_threadMuting_create], ['notes/thread-muting/delete', ep___notes_threadMuting_delete], diff --git a/packages/backend/src/server/api/endpoints/notes/shuffled.ts b/packages/backend/src/server/api/endpoints/notes/shuffled.ts new file mode 100644 index 000000000..a5c96b3d7 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/shuffled.ts @@ -0,0 +1,80 @@ +import { Notes } from '@/models/index.js'; +import { activeUsersChart } from '@/services/chart/index.js'; +import define from '@/server/api/define.js'; +import { makePaginationQuery } from '@/server/api/common/make-pagination-query.js'; +import { visibilityQuery } from '@/server/api/common/generate-visibility-query.js'; +import { generateMutedUserQuery } from '@/server/api/common/generate-muted-user-query.js'; +import { generateRepliesQuery } from '@/server/api/common/generate-replies-query.js'; +import { generateMutedNoteQuery } from '@/server/api/common/generate-muted-note-query.js'; +import { generateChannelQuery } from '@/server/api/common/generate-channel-query.js'; +import { generateBlockedUserQuery } from '@/server/api/common/generate-block-query.js'; +import { generateMutedRenotesQuery } from '@/server/api/common/generated-muted-renote-query.js'; + +export const meta = { + tags: ['notes'], + + description: 'Returns a shuffled list of the current users notes.', + + requireCredential: true, + + 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 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + seed: { + description: 'A seed for the shuffling. If the same seed is provided, the same shuffling will be used. Required for pagination to work.', + type: 'number', + }, + }, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, user) => { + //#region Construct query + const query = makePaginationQuery(Notes.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .addSelect('md5(note.id || ' + ps.seed + ')', 'shuffleorder') + .andWhere('note.userId = :meId', { meId: user.id }) + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('user.avatar', 'avatar') + .leftJoinAndSelect('user.banner', 'banner') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar') + .leftJoinAndSelect('replyUser.banner', 'replyUserBanner') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar') + .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner') + .orderBy('shuffleorder'); + + generateChannelQuery(query, user); + generateRepliesQuery(query, user); + generateMutedUserQuery(query, user); + generateMutedNoteQuery(query, user); + generateBlockedUserQuery(query, user); + generateMutedRenotesQuery(query, user); + //#endregion + + const timeline = await visibilityQuery(query, user).take(ps.limit).getMany(); + + process.nextTick(() => { + activeUsersChart.read(user); + }); + + return await Notes.packMany(timeline, user); +}); diff --git a/packages/client/src/components/timeline.vue b/packages/client/src/components/timeline.vue index 147c0c8a0..b60793420 100644 --- a/packages/client/src/components/timeline.vue +++ b/packages/client/src/components/timeline.vue @@ -10,7 +10,7 @@ import * as sound from '@/scripts/sound'; import { $i } from '@/account'; const props = defineProps<{ - src: 'antenna' | 'home' | 'local' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'channel' | 'file'; + src: 'antenna' | 'home' | 'local' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'channel' | 'file' | 'shuffled'; list?: string; antenna?: string; channel?: string; @@ -51,6 +51,7 @@ const onChangeFollowing = () => { } }; +let randomSeed = Math.random(); let endpoint; let query; let connection; @@ -136,6 +137,12 @@ switch (props.src) { fileId: props.fileId, }; break; + case 'shuffled': + endpoint = 'notes/shuffled'; + query = { + seed: randomSeed, + }; + break; } const pagination = { diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue index cdc99af4d..fcafcca95 100644 --- a/packages/client/src/pages/timeline.vue +++ b/packages/client/src/pages/timeline.vue @@ -144,6 +144,11 @@ const headerTabs = $computed(() => [{ title: i18n.ts.channel, iconOnly: true, onClick: chooseChannel, +}, { + key: 'shuffled', + icon: 'fas fa-shuffle', + title: i18n.ts._timelines.shuffled, + iconOnly: true, }]); definePageMetadata(computed(() => ({