From 44a66c48b7d12a230930402588957c39b5e048e3 Mon Sep 17 00:00:00 2001 From: Esurio/1673beta <60435625+1673beta@users.noreply.github.com> Date: Tue, 9 Jul 2024 20:35:51 +0900 Subject: [PATCH] =?UTF-8?q?feat:=E3=80=80=E3=82=BF=E3=82=A4=E3=83=A0?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=B3=E3=81=8B=E3=82=89=E6=8A=95=E7=A8=BF?= =?UTF-8?q?=E3=82=92=E9=99=A4=E5=A4=96=E3=81=99=E3=82=8B=E3=82=AA=E3=83=97?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [WIP]feat: withoutBots * feat: withoutBot * FTT有効時に想定の真逆の挙動をとるのを修正 --- .../src/core/FanoutTimelineEndpointService.ts | 6 ++++++ .../server/api/endpoints/channels/timeline.ts | 1 + .../api/endpoints/notes/global-timeline.ts | 5 +++++ .../api/endpoints/notes/hybrid-timeline.ts | 9 ++++++++ .../api/endpoints/notes/local-timeline.ts | 9 ++++++++ .../server/api/endpoints/notes/timeline.ts | 21 ++++++++++++++++++- .../src/server/api/endpoints/users/notes.ts | 9 ++++++++ .../api/stream/channels/global-timeline.ts | 3 +++ .../api/stream/channels/home-timeline.ts | 3 +++ .../api/stream/channels/hybrid-timeline.ts | 3 +++ .../api/stream/channels/local-timeline.ts | 3 +++ .../cherrypick-js/etc/cherrypick-js.api.md | 4 ++++ packages/cherrypick-js/src/autogen/types.ts | 10 +++++++++ packages/cherrypick-js/src/streaming.types.ts | 4 ++++ .../frontend/src/components/MkTimeline.vue | 14 ++++++++++++- packages/frontend/src/pages/timeline.vue | 12 ++++++++++- packages/frontend/src/store.ts | 1 + 17 files changed, 114 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index e3dc94e4f8..91c471ea96 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -34,6 +34,7 @@ type TimelineOptions = { excludeReplies?: boolean; excludePureRenotes: boolean; withCats: boolean; + withoutBots: boolean; dbFallback: (untilId: string | null, sinceId: string | null, limit: number) => Promise, }; @@ -104,6 +105,11 @@ export class FanoutTimelineEndpointService { filter = (note) => (note.user ? note.user.isCat : false) && parentFilter(note); } + if (ps.withoutBots) { + const parentFilter = filter; + filter = (note) => (!note.user || !note.user.isBot) && parentFilter(note); + } + if (ps.me) { const me = ps.me; const [ diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index bd6be1783f..a2de0d570b 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -102,6 +102,7 @@ export default class extends Endpoint { // eslint- redisTimelines: [`channelTimeline:${channel.id}`], excludePureRenotes: false, withCats: false, + withoutBots: false, dbFallback: async (untilId, sinceId, limit) => { return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me); }, diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 5e3552377a..91feb48e03 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -42,6 +42,7 @@ export const paramDef = { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withCats: { type: 'boolean', default: false }, + withoutBots: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -102,6 +103,10 @@ export default class extends Endpoint { // eslint- if (ps.withCats) { query.andWhere('(select "isCat" from "user" where id = note."userId")'); } + + if (ps.withoutBots) { + query.andWhere('(SELECT "isBot" FROM "user" WHERE id = note."userId") = FALSE'); + } //#endregion const timeline = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 6e406dc6b0..67e5ce2e6c 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -68,6 +68,7 @@ export const paramDef = { withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, withCats: { type: 'boolean', default: false }, + withoutBots: { type: 'boolean', default: false }, }, required: [], } as const; @@ -115,6 +116,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withReplies: ps.withReplies, withCats: ps.withCats, + withoutBots: ps.withoutBots, }, me); process.nextTick(() => { @@ -155,6 +157,7 @@ export default class extends Endpoint { // eslint- alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, withCats: ps.withCats, + withoutBots: ps.withoutBots, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ untilId, sinceId, @@ -165,6 +168,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withReplies: ps.withReplies, withCats: ps.withCats, + withoutBots: ps.withoutBots, }, me), }); @@ -186,6 +190,7 @@ export default class extends Endpoint { // eslint- withFiles: boolean, withReplies: boolean, withCats: boolean, + withoutBots: boolean, }, me: MiLocalUser) { const followees = await this.userFollowingService.getFollowees(me.id); const followingChannels = await this.channelFollowingsRepository.find({ @@ -276,6 +281,10 @@ export default class extends Endpoint { // eslint- if (ps.withCats) { query.andWhere('(select "isCat" from "user" where id = note."userId")'); } + + if (ps.withoutBots) { + query.andWhere('(SELECT "isBot" FROM "user" WHERE id = note."userId") = FALSE'); + } //#endregion return await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 60ba408f4d..838e8d49b3 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -54,6 +54,7 @@ export const paramDef = { withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, withCats: { type: 'boolean', default: false }, + withoutBots: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -100,6 +101,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withReplies: ps.withReplies, withCats: ps.withCats, + withoutBots: ps.withoutBots, }, me); process.nextTick(() => { @@ -126,6 +128,7 @@ export default class extends Endpoint { // eslint- alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, withCats: ps.withCats, + withoutBots: ps.withoutBots, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ untilId, sinceId, @@ -133,6 +136,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withReplies: ps.withReplies, withCats: ps.withCats, + withoutBots: ps.withoutBots, }, me), }); @@ -153,6 +157,7 @@ export default class extends Endpoint { // eslint- withFiles: boolean, withReplies: boolean, withCats: boolean, + withoutBots: boolean, }, me: MiLocalUser | null) { const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) @@ -188,6 +193,10 @@ export default class extends Endpoint { // eslint- query.andWhere('(select "isCat" from "user" where id = note."userId")'); } + if (ps.withoutBots) { + query.andWhere('(SELECT "isBot" FROM "user" WHERE id = note."userId") = FALSE'); + } + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 11a2be2723..a6b6cc9150 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -50,6 +50,7 @@ export const paramDef = { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withCats: { type: 'boolean', default: false }, + withoutBots: { type: 'boolean', default: false }, }, required: [], } as const; @@ -89,6 +90,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withCats: ps.withCats, + withoutBots: ps.withoutBots, }, me); process.nextTick(() => { @@ -115,6 +117,7 @@ export default class extends Endpoint { // eslint- alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, withCats: ps.withCats, + withoutBots: ps.withoutBots, noteFilter: note => { if (note.reply && note.reply.visibility === 'followers') { if (!Object.hasOwn(followings, note.reply.userId)) return false; @@ -132,6 +135,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withCats: ps.withCats, + withoutBots: ps.withoutBots, }, me), }); @@ -143,7 +147,18 @@ export default class extends Endpoint { // eslint- }); } - private async getFromDb(ps: { untilId: string | null; sinceId: string | null; limit: number; includeMyRenotes: boolean; includeRenotedMyNotes: boolean; includeLocalRenotes: boolean; withFiles: boolean; withRenotes: boolean; withCats: boolean; }, me: MiLocalUser) { + private async getFromDb(ps: { + untilId: string | null; + sinceId: string | null; + limit: number; + includeMyRenotes: boolean; + includeRenotedMyNotes: boolean; + includeLocalRenotes: boolean; + withFiles: boolean; + withRenotes: boolean; + withCats: boolean; + withoutBots: boolean; + }, me: MiLocalUser) { const followees = await this.userFollowingService.getFollowees(me.id); const followingChannels = await this.channelFollowingsRepository.find({ where: { @@ -249,6 +264,10 @@ export default class extends Endpoint { // eslint- if (ps.withCats) { query.andWhere('(select "isCat" from "user" where id = note."userId")'); } + + if (ps.withoutBots) { + query.andWhere('(SELECT "isBot" FROM "user" WHERE id = note."userId") = FALSE'); + } //#endregion return await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index f1b34ced5f..e1f2a13802 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -53,6 +53,7 @@ export const paramDef = { withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withChannelNotes: { type: 'boolean', default: false }, + withoutBots: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -105,6 +106,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withCats: ps.withCats, + withoutBots: ps.withoutBots, }, me); return await this.noteEntityService.packMany(timeline, me); @@ -130,6 +132,7 @@ export default class extends Endpoint { // eslint- excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files excludePureRenotes: !ps.withRenotes, withCats: ps.withCats, + withoutBots: ps.withoutBots, 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; @@ -146,6 +149,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withCats: ps.withCats, + withoutBots: ps.withoutBots, }, me), }); @@ -162,6 +166,7 @@ export default class extends Endpoint { // eslint- withFiles: boolean, withCats: boolean, withRenotes: boolean, + withoutBots: boolean, }, me: MiLocalUser | null) { const isSelf = me && (me.id === ps.userId); @@ -207,6 +212,10 @@ export default class extends Endpoint { // eslint- query.andWhere('(select "isCat" from "user" where id = note."userId")'); } + if (ps.withoutBots) { + query.andWhere('(SELECT "isBot" FROM "user" WHERE id = note."userId") = FALSE'); + } + return await query.limit(ps.limit).getMany(); } } diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 17116258d8..ad63f070df 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -18,6 +18,7 @@ class GlobalTimelineChannel extends Channel { public static requireCredential = false as const; private withRenotes: boolean; private withFiles: boolean; + private withoutBots: boolean; constructor( private metaService: MetaService, @@ -38,6 +39,7 @@ class GlobalTimelineChannel extends Channel { this.withRenotes = params.withRenotes ?? true; this.withFiles = params.withFiles ?? false; + this.withoutBots = params.withoutBots ?? false; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -46,6 +48,7 @@ class GlobalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (this.withoutBots && note.user.isBot) return; if (note.visibility !== 'public') return; if (note.channelId != null) return; diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 878a3180cb..82c077da9b 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -17,6 +17,7 @@ class HomeTimelineChannel extends Channel { public static kind = 'read:account'; private withRenotes: boolean; private withFiles: boolean; + private withoutBots: boolean; constructor( private noteEntityService: NoteEntityService, @@ -32,6 +33,7 @@ class HomeTimelineChannel extends Channel { public async init(params: any) { this.withRenotes = params.withRenotes ?? true; this.withFiles = params.withFiles ?? false; + this.withoutBots = params.withoutBots ?? false; this.subscriber.on('notesStream', this.onNote); } @@ -41,6 +43,7 @@ class HomeTimelineChannel extends Channel { const isMe = this.user!.id === note.userId; if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (this.withoutBots && note.user.isBot) return; if (note.channelId) { if (!this.followingChannels.has(note.channelId)) return; diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 889ee0c971..65a120e9e1 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -20,6 +20,7 @@ class HybridTimelineChannel extends Channel { private withRenotes: boolean; private withReplies: boolean; private withFiles: boolean; + private withoutBots: boolean; constructor( private metaService: MetaService, @@ -41,6 +42,7 @@ class HybridTimelineChannel extends Channel { this.withRenotes = params.withRenotes ?? true; this.withReplies = params.withReplies ?? false; this.withFiles = params.withFiles ?? false; + this.withoutBots = params.withoutBots ?? false; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -51,6 +53,7 @@ class HybridTimelineChannel extends Channel { const isMe = this.user!.id === note.userId; if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (this.withoutBots && note.user.isBot) return; // チャンネルの投稿ではなく、自分自身の投稿 または // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 442d08ae51..9fe642ec0c 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -19,6 +19,7 @@ class LocalTimelineChannel extends Channel { private withRenotes: boolean; private withReplies: boolean; private withFiles: boolean; + private withoutBots: boolean; constructor( private metaService: MetaService, @@ -40,6 +41,7 @@ class LocalTimelineChannel extends Channel { this.withRenotes = params.withRenotes ?? true; this.withReplies = params.withReplies ?? false; this.withFiles = params.withFiles ?? false; + this.withoutBots = params.withoutBots ?? false; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -48,6 +50,7 @@ class LocalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (this.withoutBots && note.user.isBot) return; if (note.user.host !== null) return; if (note.visibility !== 'public') return; diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index 313f74c577..189de726f7 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -652,6 +652,7 @@ export type Channels = { withRenotes?: boolean; withFiles?: boolean; withCats?: boolean; + withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -664,6 +665,7 @@ export type Channels = { withReplies?: boolean; withFiles?: boolean; withCats?: boolean; + withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -676,6 +678,7 @@ export type Channels = { withReplies?: boolean; withFiles?: boolean; withCats?: boolean; + withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -687,6 +690,7 @@ export type Channels = { withRenotes?: boolean; withFiles?: boolean; withCats?: boolean; + withoutBots?: boolean; }; events: { note: (payload: Note) => void; diff --git a/packages/cherrypick-js/src/autogen/types.ts b/packages/cherrypick-js/src/autogen/types.ts index 0bd8a16c01..529943ea30 100644 --- a/packages/cherrypick-js/src/autogen/types.ts +++ b/packages/cherrypick-js/src/autogen/types.ts @@ -22851,6 +22851,8 @@ export type operations = { withRenotes?: boolean; /** @default false */ withCats?: boolean; + /** @default false */ + withoutBots?: boolean; /** @default 10 */ limit?: number; /** Format: misskey:id */ @@ -22935,6 +22937,8 @@ export type operations = { withReplies?: boolean; /** @default false */ withCats?: boolean; + /** @default false */ + withoutBots?: boolean; }; }; }; @@ -22995,6 +22999,8 @@ export type operations = { withReplies?: boolean; /** @default false */ withCats?: boolean; + /** @default false */ + withoutBots?: boolean; /** @default 10 */ limit?: number; /** Format: misskey:id */ @@ -24102,6 +24108,8 @@ export type operations = { withRenotes?: boolean; /** @default false */ withCats?: boolean; + /** @default false */ + withoutBots?: boolean; }; }; }; @@ -28383,6 +28391,8 @@ export type operations = { withRenotes?: boolean; /** @default false */ withChannelNotes?: boolean; + /** @default false */ + withoutBots?: boolean; /** @default 10 */ limit?: number; /** Format: misskey:id */ diff --git a/packages/cherrypick-js/src/streaming.types.ts b/packages/cherrypick-js/src/streaming.types.ts index cf2591b017..9de6b53940 100644 --- a/packages/cherrypick-js/src/streaming.types.ts +++ b/packages/cherrypick-js/src/streaming.types.ts @@ -71,6 +71,7 @@ export type Channels = { withRenotes?: boolean; withFiles?: boolean; withCats?: boolean; + withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -83,6 +84,7 @@ export type Channels = { withReplies?: boolean; withFiles?: boolean; withCats?: boolean; + withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -95,6 +97,7 @@ export type Channels = { withReplies?: boolean; withFiles?: boolean; withCats?: boolean; + withoutBots?: boolean; }; events: { note: (payload: Note) => void; @@ -106,6 +109,7 @@ export type Channels = { withRenotes?: boolean; withFiles?: boolean; withCats?: boolean; + withoutBots?: boolean; }; events: { note: (payload: Note) => void; diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 34e0e83126..17a6eff066 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -41,11 +41,13 @@ const props = withDefaults(defineProps<{ withReplies?: boolean; onlyFiles?: boolean; onlyCats?: boolean; + withoutBots?: boolean; }>(), { withRenotes: true, withReplies: false, onlyFiles: false, onlyCats: false, + withoutBots: false, }); const emit = defineEmits<{ @@ -62,6 +64,7 @@ type TimelineQueryType = { withReplies?: boolean, withFiles?: boolean, withCats?: boolean, + withoutBots?: boolean, visibility?: string, listId?: string, channelId?: string, @@ -109,6 +112,7 @@ function connectChannel() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, withCats: props.onlyCats, + withoutBots: props.withoutBots, }); connection2 = stream.useChannel('main'); } else if (props.src === 'local') { @@ -117,13 +121,14 @@ function connectChannel() { withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, withCats: props.onlyCats, + withoutBots: props.withoutBots, }); } else if (props.src === 'media') { connection = stream.useChannel('globalTimeline', { withRenotes: props.withRenotes, - withReplies: props.withReplies, withFiles: true, withCats: props.onlyCats, + withoutBots: props.withoutBots, }, ); } else if (props.src === 'social') { @@ -132,12 +137,14 @@ function connectChannel() { withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, withCats: props.onlyCats, + withoutBots: props.withoutBots, }); } else if (props.src === 'global') { connection = stream.useChannel('globalTimeline', { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, withCats: props.onlyCats, + withoutBots: props.withoutBots, }); } else if (props.src === 'mentions') { connection = stream.useChannel('main'); @@ -192,6 +199,7 @@ function updatePaginationQuery() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, withCats: props.onlyCats, + withoutBots: props.withoutBots, }; } else if (props.src === 'local') { endpoint = 'notes/local-timeline'; @@ -200,6 +208,7 @@ function updatePaginationQuery() { withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, withCats: props.onlyCats, + withoutBots: props.withoutBots, }; } else if (props.src === 'media') { endpoint = 'notes/global-timeline'; @@ -208,6 +217,7 @@ function updatePaginationQuery() { withReplies: props.withReplies, withFiles: true, withCats: props.onlyCats, + withoutBots: props.withoutBots, }; } else if (props.src === 'social') { endpoint = 'notes/hybrid-timeline'; @@ -216,6 +226,7 @@ function updatePaginationQuery() { withReplies: props.withReplies, withFiles: props.onlyFiles ? true : undefined, withCats: props.onlyCats, + withoutBots: props.withoutBots, }; } else if (props.src === 'global') { endpoint = 'notes/global-timeline'; @@ -223,6 +234,7 @@ function updatePaginationQuery() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, withCats: props.onlyCats, + withoutBots: props.withoutBots, }; } else if (props.src === 'mentions') { endpoint = 'notes/mentions'; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 7ecac3ac3b..654e23e732 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -39,13 +39,14 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -140,6 +141,11 @@ const onlyCats = computed({ set: (x: boolean) => saveTlFilter('onlyCats', x), }); +const withoutBots = computed({ + get: () => defaultStore.reactiveState.tl.value.filter.withoutBots, + set: (x: boolean) => saveTlFilter('withoutBots', x), +}); + watch([withReplies, onlyFiles], ([withRepliesTo, onlyFilesTo]) => { if (withRepliesTo) { localSocialTLFilterSwitchStore.value = 'withReplies'; @@ -346,6 +352,10 @@ const headerActions = computed(() => { type: 'switch', text: i18n.ts.showCatOnly, ref: onlyCats, + }, { + type: 'switch', + text: i18n.ts.antennaExcludeBots, + ref: withoutBots, }], ev.currentTarget ?? ev.target); }, }, diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 27de24d449..a5ae0e4641 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -200,6 +200,7 @@ export const defaultStore = markRaw(new Storage('base', { withSensitive: true, onlyFiles: false, onlyCats: false, + withoutBots: false, }, }, },