From 463b9ac59def86dd1b9065cbe7382325c1e5824e Mon Sep 17 00:00:00 2001 From: Hazel K Date: Wed, 9 Oct 2024 15:09:55 -0400 Subject: add filters for following feed --- .../src/server/api/endpoints/users/notes.ts | 50 +++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) (limited to 'packages/backend/src/server/api/endpoints/users') diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index cc76c12f1d..884760a88f 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -17,6 +17,7 @@ import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { ApiError } from '@/server/api/error.js'; +import { isQuote, isRenote } from '@/misc/is-renote.js'; export const meta = { tags: ['users', 'notes'], @@ -51,7 +52,10 @@ export const paramDef = { properties: { userId: { type: 'string', format: 'misskey:id' }, withReplies: { type: 'boolean', default: false }, + withRepliesToSelf: { type: 'boolean', default: true }, + withQuotes: { type: 'boolean', default: true }, withRenotes: { type: 'boolean', default: true }, + withNonPublic: { type: 'boolean', default: true }, withChannelNotes: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, @@ -103,6 +107,10 @@ export default class extends Endpoint { // eslint- withChannelNotes: ps.withChannelNotes, withFiles: ps.withFiles, withRenotes: ps.withRenotes, + withQuotes: ps.withQuotes, + withNonPublic: ps.withNonPublic, + withRepliesToOthers: ps.withReplies, + withRepliesToSelf: ps.withRepliesToSelf, }, me); return await this.noteEntityService.packMany(timeline, me); @@ -132,6 +140,11 @@ export default class extends Endpoint { // eslint- if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; if (note.visibility === 'followers' && !isFollowing && !isSelf) return false; + // These are handled by DB fallback, but we duplicate them here in case a timeline was already populated with notes + if (!ps.withRepliesToSelf && note.reply?.userId === note.userId) return false; + if (!ps.withQuotes && isRenote(note) && isQuote(note)) return false; + if (!ps.withNonPublic && note.visibility !== 'public') return false; + return true; }, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ @@ -142,6 +155,10 @@ export default class extends Endpoint { // eslint- withChannelNotes: ps.withChannelNotes, withFiles: ps.withFiles, withRenotes: ps.withRenotes, + withQuotes: ps.withQuotes, + withNonPublic: ps.withNonPublic, + withRepliesToOthers: ps.withReplies, + withRepliesToSelf: ps.withRepliesToSelf, }, me), }); @@ -157,6 +174,10 @@ export default class extends Endpoint { // eslint- withChannelNotes: boolean, withFiles: boolean, withRenotes: boolean, + withQuotes: boolean, + withNonPublic: boolean, + withRepliesToOthers: boolean, + withRepliesToSelf: boolean, }, me: MiLocalUser | null) { const isSelf = me && (me.id === ps.userId); @@ -188,7 +209,9 @@ export default class extends Endpoint { // eslint- query.andWhere('note.fileIds != \'{}\''); } - if (ps.withRenotes === false) { + if (!ps.withRenotes && !ps.withQuotes) { + query.andWhere('note.renoteId IS NULL'); + } else if (!ps.withRenotes) { query.andWhere(new Brackets(qb => { qb.orWhere('note.userId != :userId', { userId: ps.userId }); qb.orWhere('note.renoteId IS NULL'); @@ -196,6 +219,31 @@ export default class extends Endpoint { // eslint- qb.orWhere('note.fileIds != \'{}\''); qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); })); + } else if (!ps.withQuotes) { + query.andWhere(` + ( + note."renoteId" IS NULL + OR ( + note.text IS NULL + AND note.cw IS NULL + AND note."replyId" IS NULL + AND note."hasPoll" IS FALSE + AND note."fileIds" = '{}' + ) + ) + `); + } + + if (!ps.withRepliesToOthers && !ps.withRepliesToSelf) { + query.andWhere('reply.id IS NULL'); + } else if (!ps.withRepliesToOthers) { + query.andWhere('(reply.id IS NULL OR reply."userId" = note."userId")'); + } else if (!ps.withRepliesToSelf) { + query.andWhere('(reply.id IS NULL OR reply."userId" != note."userId")'); + } + + if (!ps.withNonPublic) { + query.andWhere('note.visibility = \'public\''); } return await query.limit(ps.limit).getMany(); -- cgit v1.2.3-freya From 9b1bae653d96fd276bbebde7c9dab3adf6236a77 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 12 Oct 2024 14:56:04 -0400 Subject: add "show bots" toggle to following feed --- .../src/server/api/endpoints/notes/following.ts | 6 ++++++ .../backend/src/server/api/endpoints/users/notes.ts | 9 +++++++++ .../frontend/src/components/SkUserRecentNotes.vue | 1 + packages/frontend/src/pages/following-feed.vue | 21 ++++++++++++++------- packages/frontend/src/store.ts | 1 + packages/misskey-js/src/autogen/types.ts | 4 ++++ 6 files changed, 35 insertions(+), 7 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/users') diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts index a75a928009..83e8f404e9 100644 --- a/packages/backend/src/server/api/endpoints/notes/following.ts +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -37,6 +37,7 @@ export const paramDef = { includeNonPublic: { type: 'boolean', default: false }, includeReplies: { type: 'boolean', default: false }, includeQuotes: { type: 'boolean', default: false }, + includeBots: { type: 'boolean', default: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, @@ -97,6 +98,11 @@ export default class extends Endpoint { // eslint- query.andWhere('latest.is_quote = false'); } + // Match selected user types. + if (!ps.includeBots) { + query.andWhere('"user"."isBot" = false'); + } + // Respect blocks and mutes this.queryService.generateBlockedUserQuery(query, me); this.queryService.generateMutedUserQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 884760a88f..efea15ca80 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -55,6 +55,7 @@ export const paramDef = { withRepliesToSelf: { type: 'boolean', default: true }, withQuotes: { type: 'boolean', default: true }, withRenotes: { type: 'boolean', default: true }, + withBots: { type: 'boolean', default: true }, withNonPublic: { type: 'boolean', default: true }, withChannelNotes: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, @@ -108,6 +109,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withQuotes: ps.withQuotes, + withBots: ps.withBots, withNonPublic: ps.withNonPublic, withRepliesToOthers: ps.withReplies, withRepliesToSelf: ps.withRepliesToSelf, @@ -135,6 +137,7 @@ export default class extends Endpoint { // eslint- excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files excludePureRenotes: !ps.withRenotes, + excludeBots: !ps.withBots, noteFilter: note => { if (note.channel?.isSensitive && !isSelf) return false; if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; @@ -156,6 +159,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withQuotes: ps.withQuotes, + withBots: ps.withBots, withNonPublic: ps.withNonPublic, withRepliesToOthers: ps.withReplies, withRepliesToSelf: ps.withRepliesToSelf, @@ -175,6 +179,7 @@ export default class extends Endpoint { // eslint- withFiles: boolean, withRenotes: boolean, withQuotes: boolean, + withBots: boolean, withNonPublic: boolean, withRepliesToOthers: boolean, withRepliesToSelf: boolean, @@ -246,6 +251,10 @@ export default class extends Endpoint { // eslint- query.andWhere('note.visibility = \'public\''); } + if (!ps.withBots) { + query.andWhere('"user"."isBot" = false'); + } + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/frontend/src/components/SkUserRecentNotes.vue b/packages/frontend/src/components/SkUserRecentNotes.vue index 31580075ef..2cdb4b6586 100644 --- a/packages/frontend/src/components/SkUserRecentNotes.vue +++ b/packages/frontend/src/components/SkUserRecentNotes.vue @@ -29,6 +29,7 @@ const props = defineProps<{ withNonPublic: boolean; withQuotes: boolean; withReplies: boolean; + withBots: boolean; onlyFiles: boolean; }>(); diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index 1b3f303dfe..7b90b563e6 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -30,18 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
- -