diff --git a/CHANGELOG-VRTL.md b/CHANGELOG-VRTL.md new file mode 100644 index 000000000000..fdbe020749ba --- /dev/null +++ b/CHANGELOG-VRTL.md @@ -0,0 +1,13 @@ +# CHANGELOG about VRTL + +VRTLのブランチで行われた変更点をまとめています + + +- chore(backend): VRTL参加サーバーの取得に失敗したときのリトライの間隔を短く +- feat: VRTL/VSTLに連合なし投稿を含めるかを選択可能に + - もともとのVRTL/VSTLでは連合なし投稿が常に含まれていましたが、正しくVRTL/VSTLのノートを表現するために含めないようにできるようになりました + - VSTLの場合、連合なし投稿を含めないようにしてもフォローしている人の連合なし投稿は表示されます +- fix(frontend): ウィジェットでVRTL/VSTLが使用できない問題を修正 +- fix(backend): 自分自身に対するリプライがwithReplies = falseなVRTL/VSTLにて含まれていない問題を修正 +- feat(backend): `vmimiRelayTimelineImplemented` と `disableVmimiRelayTimeline` nodeinfo に追加しました + - これによりサードパーティクライアントがVRTLの有無を認知できるようになりました。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1397efc76f40..35a2364ef002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## Unreleased + +### General +- Feat: VRTL/VSTLに連合なし投稿を含めるかを選択可能に + - もともとのVRTL/VSTLでは連合なし投稿が常に含まれていましたが、正しくVRTL/VSTLのノートを表現するために含めないようにできるようになりました + - VSTLの場合、連合なし投稿を含めないようにしてもフォローしている人の連合なし投稿は表示されます + +### Client +- Fix: ウィジェットでVRTL/VSTLが使用できない問題を修正 + +### Server +- Enhance: `vmimiRelayTimelineImplemented` と `disableVmimiRelayTimeline` nodeinfo に追加しました + - これによりサードパーティクライアントがVRTLの有無を認知できるようになりました。 +- Enhance: VRTL参加サーバーの取得に失敗したときのリトライの間隔を短く +- Fix: 自分自身に対するリプライがwithReplies = falseなVRTL/VSTLにて含まれていない問題を修正 + ## 2024.5.0-kinel.1 ### General diff --git a/locales/index.d.ts b/locales/index.d.ts index ad0b62dbd513..b4830a403d9e 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4732,6 +4732,10 @@ export interface Locale extends ILocale { * TLに現在フォロー中の人全員の返信を含めないようにする */ "hideRepliesToOthersInTimelineAll": string; + /** + * TLに連合なし投稿を含める + */ + "showLocalOnlyInTimeline": string; /** * この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか? */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f40f86b5a5af..d5c7841ace4b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1179,6 +1179,7 @@ showRepliesToOthersInTimeline: "TLに他の人への返信を含める" hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない" showRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めるようにする" hideRepliesToOthersInTimelineAll: "TLに現在フォロー中の人全員の返信を含めないようにする" +showLocalOnlyInTimeline: "TLに連合なし投稿を含める" confirmShowRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めるようにしますか?" confirmHideRepliesAll: "この操作は元に戻せません。本当にTLに現在フォロー中の人全員の返信を含めないようにしますか?" externalServices: "外部サービス" diff --git a/packages/backend/src/core/FanoutTimelineService.ts b/packages/backend/src/core/FanoutTimelineService.ts index d0e21aec9c66..8c9212418b4b 100644 --- a/packages/backend/src/core/FanoutTimelineService.ts +++ b/packages/backend/src/core/FanoutTimelineService.ts @@ -42,6 +42,7 @@ export type FanoutTimelineName = | 'vmimiRelayTimeline' // replies are not included | 'vmimiRelayTimelineWithFiles' // only non-reply notes with files are included | 'vmimiRelayTimelineWithReplies' // only replies are included + | `vmimiRelayTimelineWithReplyTo:${string}` // Only replies to specific local user are included. Parameter is reply user id. @Injectable() export class FanoutTimelineService { diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index d1b2b05c1255..c497567e5883 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -972,8 +972,11 @@ export class NoteCreateService implements OnApplicationShutdown { this.fanoutTimelineService.push(`localTimelineWithReplyTo:${note.replyUserId}`, note.id, 300 / 10, r); } } - if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost)) { + if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost) && !note.localOnly) { this.fanoutTimelineService.push('vmimiRelayTimelineWithReplies', note.id, meta.vmimiRelayTimelineCacheMax, r); + if (note.replyUserHost == null) { + this.fanoutTimelineService.push(`vmimiRelayTimelineWithReplyTo:${note.replyUserId}`, note.id, meta.vmimiRelayTimelineCacheMax / 10, r); + } } } else { this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); @@ -987,7 +990,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r); } } - if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost)) { + if (note.visibility === 'public' && this.vmimiRelayTimelineService.isRelayedInstance(note.userHost) && !note.localOnly) { this.fanoutTimelineService.push('vmimiRelayTimeline', note.id, meta.vmimiRelayTimelineCacheMax, r); if (note.fileIds.length > 0) { this.fanoutTimelineService.push('vmimiRelayTimelineWithFiles', note.id, meta.vmimiRelayTimelineCacheMax / 2, r); diff --git a/packages/backend/src/core/VmimiRelayTimelineService.ts b/packages/backend/src/core/VmimiRelayTimelineService.ts index 6dd9c5efcaa6..a5f1b43699cc 100644 --- a/packages/backend/src/core/VmimiRelayTimelineService.ts +++ b/packages/backend/src/core/VmimiRelayTimelineService.ts @@ -13,14 +13,16 @@ import type Logger from '@/logger.js'; type VmimiInstanceList = { Url: string; }[]; // one day -const UpdateInterval = 1000 * 60 * 60 * 24; -const RetryInterval = 1000 * 60 * 60 * 6; +const UpdateInterval = 1000 * 60 * 60 * 24; // 24 hours = 1 day +const MinRetryInterval = 1000 * 60; // one minutes +const MaxRetryInterval = 1000 * 60 * 60 * 6; // 6 hours @Injectable() export class VmimiRelayTimelineService { instanceHosts: Set; instanceHostsArray: string[]; nextUpdate: number; + nextRetryInterval: number; updatePromise: Promise | null; private logger: Logger; @@ -32,6 +34,7 @@ export class VmimiRelayTimelineService { this.instanceHosts = new Set([]); this.instanceHostsArray = []; this.nextUpdate = 0; + this.nextRetryInterval = MinRetryInterval; this.updatePromise = null; this.logger = this.loggerService.getLogger('vmimi'); @@ -55,10 +58,12 @@ export class VmimiRelayTimelineService { this.instanceHosts = new Set(this.instanceHostsArray); this.nextUpdate = Date.now() + UpdateInterval; this.logger.info(`Got instance list: ${this.instanceHostsArray}`); + this.nextRetryInterval = MinRetryInterval; } catch (e) { this.logger.error('Failed to update instance list', e as any); - this.nextUpdate = Date.now() + RetryInterval; - setTimeout(() => this.checkForUpdateInstanceList(), RetryInterval + 5); + this.nextUpdate = Date.now() + this.nextRetryInterval; + setTimeout(() => this.checkForUpdateInstanceList(), this.nextRetryInterval + 5); + this.nextRetryInterval = Math.min(this.nextRetryInterval * 2, MaxRetryInterval); } } diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index cc18997fdc1c..000d7f0ba4f4 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -115,6 +115,8 @@ export class NodeinfoServerService { disableRegistration: meta.disableRegistration, disableLocalTimeline: !basePolicies.ltlAvailable, disableGlobalTimeline: !basePolicies.gtlAvailable, + vmimiRelayTimelineImplemented: true, + disableVmimiRelayTimeline: !basePolicies.vrtlAvailable, emailRequiredForSignup: meta.emailRequiredForSignup, enableHcaptcha: meta.enableHcaptcha, enableRecaptcha: meta.enableRecaptcha, diff --git a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-hybrid-timeline.ts index bd1c8acd2f38..c72b2ec83ac8 100644 --- a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-hybrid-timeline.ts @@ -58,6 +58,7 @@ export const paramDef = { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, + withLocalOnly: { type: 'boolean', default: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, allowPartial: { type: 'boolean', default: true }, // this timeline is new so true by default sinceId: { type: 'string', format: 'misskey:id' }, @@ -108,6 +109,7 @@ export default class extends Endpoint { // eslint- limit: ps.limit, withFiles: ps.withFiles, withReplies: ps.withReplies, + withLocalOnly: ps.withLocalOnly, }, me); process.nextTick(() => { @@ -124,17 +126,21 @@ export default class extends Endpoint { // eslint- `homeTimelineWithFiles:${me.id}`, 'vmimiRelayTimelineWithFiles', ]; + if (ps.withLocalOnly) timelineConfig = [...timelineConfig, 'localTimelineWithFiles']; } else if (ps.withReplies) { timelineConfig = [ `homeTimeline:${me.id}`, 'vmimiRelayTimeline', 'vmimiRelayTimelineWithReplies', ]; + if (ps.withLocalOnly) timelineConfig = [...timelineConfig, 'localTimeline', 'localTimelineWithReplies']; } else { timelineConfig = [ `homeTimeline:${me.id}`, 'vmimiRelayTimeline', + `vmimiRelayTimelineWithReplyTo:${me.id}`, ]; + if (ps.withLocalOnly) timelineConfig = [...timelineConfig, 'localTimeline', `localTimelineWithReplyTo:${me.id}`]; } const [ @@ -166,6 +172,7 @@ export default class extends Endpoint { // eslint- limit, withFiles: ps.withFiles, withReplies: ps.withReplies, + withLocalOnly: ps.withLocalOnly, }, me), }); @@ -183,6 +190,7 @@ export default class extends Endpoint { // eslint- limit: number, withFiles: boolean, withReplies: boolean, + withLocalOnly: boolean, }, me: MiLocalUser) { const followees = await this.userFollowingService.getFollowees(me.id); const followingChannels = await this.channelFollowingsRepository.find({ @@ -199,6 +207,7 @@ export default class extends Endpoint { // eslint- qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); qb.orWhere(new Brackets(qb => { qb.where('note.visibility = \'public\''); + if (!ps.withLocalOnly) qb.andWhere('note.localOnly = FALSE'); qb.andWhere(new Brackets(qb => { qb.where('note.userHost IS NULL'); if (vmimiRelayInstances.length !== 0) { @@ -210,6 +219,7 @@ export default class extends Endpoint { // eslint- qb.where('note.userId = :meId', { meId: me.id }); qb.orWhere(new Brackets(qb => { qb.where('note.visibility = \'public\''); + if (!ps.withLocalOnly) qb.andWhere('note.localOnly = FALSE'); qb.andWhere(new Brackets(qb => { qb.where('note.userHost IS NULL'); if (vmimiRelayInstances.length !== 0) { diff --git a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts index 032eaf06f714..fddf282f7a64 100644 --- a/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/vmimi-relay-timeline.ts @@ -52,6 +52,7 @@ export const paramDef = { withFiles: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, withReplies: { type: 'boolean', default: false }, + withLocalOnly: { type: 'boolean', default: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, allowPartial: { type: 'boolean', default: true }, // this timeline is new so true by default sinceId: { type: 'string', format: 'misskey:id' }, @@ -98,6 +99,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withReplies: ps.withReplies, + withLocalOnly: ps.withLocalOnly, }, me); process.nextTick(() => { @@ -117,9 +119,10 @@ export default class extends Endpoint { // eslint- me, useDbFallback: serverSettings.enableFanoutTimelineDbFallback, redisTimelines: - ps.withFiles ? ['vmimiRelayTimelineWithFiles'] - : ps.withReplies ? ['vmimiRelayTimeline', 'vmimiRelayTimelineWithReplies'] - : ['vmimiRelayTimeline'], + ps.withFiles ? ['vmimiRelayTimelineWithFiles', ...(ps.withLocalOnly ? ['localTimelineWithFiles'] as const : [])] + : ps.withReplies ? ['vmimiRelayTimeline', 'vmimiRelayTimelineWithReplies', ...(ps.withLocalOnly ? ['localTimeline', 'localTimelineWithReplies'] as const : [])] + : me ? ['vmimiRelayTimeline', `vmimiRelayTimelineWithReplyTo:${me.id}`, ...(ps.withLocalOnly ? ['localTimeline', `localTimelineWithReplyTo:${me.id}`] as const : [])] + : ['vmimiRelayTimeline', ...(ps.withLocalOnly ? ['localTimeline'] as const : [])], alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ @@ -129,6 +132,7 @@ export default class extends Endpoint { // eslint- withFiles: ps.withFiles, withRenotes: ps.withRenotes, withReplies: ps.withReplies, + withLocalOnly: ps.withLocalOnly, }, me), }); @@ -149,6 +153,7 @@ export default class extends Endpoint { // eslint- withFiles: boolean, withRenotes: boolean, withReplies: boolean, + withLocalOnly: boolean, }, me: MiLocalUser | null) { //#region Construct query const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) @@ -168,6 +173,10 @@ export default class extends Endpoint { // eslint- } })); + if (!ps.withLocalOnly) { + query.andWhere('note.localOnly = FALSE'); + } + if (!ps.withReplies) { query.andWhere(new Brackets(qb => { qb diff --git a/packages/backend/src/server/api/stream/channels/vmimi-relay-hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/vmimi-relay-hybrid-timeline.ts index 2c4ce9365b3c..e24edf53e444 100644 --- a/packages/backend/src/server/api/stream/channels/vmimi-relay-hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/vmimi-relay-hybrid-timeline.ts @@ -22,6 +22,7 @@ class VmimiRelayHybridTimelineChannel extends Channel { private withRenotes: boolean; private withReplies: boolean; private withFiles: boolean; + private withLocalOnly: boolean; constructor( private metaService: MetaService, @@ -44,6 +45,7 @@ class VmimiRelayHybridTimelineChannel extends Channel { this.withRenotes = params.withRenotes ?? true; this.withReplies = params.withReplies ?? false; this.withFiles = params.withFiles ?? false; + this.withLocalOnly = params.withLocalOnly ?? true; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -62,7 +64,7 @@ class VmimiRelayHybridTimelineChannel extends Channel { if (!( (note.channelId == null && isMe) || (note.channelId == null && Object.hasOwn(this.following, note.userId)) || - (note.channelId == null && (this.vmimiRelayTimelineService.isRelayedInstance(note.user.host) && note.visibility === 'public')) || + (note.channelId == null && (this.vmimiRelayTimelineService.isRelayedInstance(note.user.host) && note.visibility === 'public') && (this.withLocalOnly || !note.localOnly)) || (note.channelId != null && this.followingChannels.has(note.channelId)) )) return; diff --git a/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts index 3fd7068c1cc6..b07261419bea 100644 --- a/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/vmimi-relay-timeline.ts @@ -20,6 +20,7 @@ class VmimiRelayTimelineChannel extends Channel { private withRenotes: boolean; private withReplies: boolean; private withFiles: boolean; + private withLocalOnly: boolean; constructor( private metaService: MetaService, @@ -41,6 +42,7 @@ class VmimiRelayTimelineChannel extends Channel { this.withRenotes = params.withRenotes ?? true; this.withReplies = params.withReplies ?? false; this.withFiles = params.withFiles ?? false; + this.withLocalOnly = params.withLocalOnly ?? true; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -51,6 +53,7 @@ class VmimiRelayTimelineChannel extends Channel { if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; if (!this.vmimiRelayTimelineService.isRelayedInstance(note.user.host ?? null)) return; + if (!this.withLocalOnly && note.localOnly) return; if (note.visibility !== 'public') return; if (note.channelId != null) return; diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 24e43ff5b0d0..bf47fef11d34 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -38,10 +38,12 @@ const props = withDefaults(defineProps<{ withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; + withLocalOnly?: boolean; }>(), { withRenotes: true, withReplies: true, onlyFiles: false, + withLocalOnly: true, }); const emit = defineEmits<{ @@ -57,6 +59,7 @@ type TimelineQueryType = { withRenotes?: boolean, withReplies?: boolean, withFiles?: boolean, + withLocalOnly?: boolean, visibility?: string, listId?: string, channelId?: string, @@ -126,12 +129,14 @@ function connectChannel() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, withReplies: props.withReplies, + withLocalOnly: props.withLocalOnly, }); } else if (props.src === 'vmimi-relay-social') { connection = stream.useChannel('vmimiRelayHybridTimeline', { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, withReplies: props.withReplies, + withLocalOnly: props.withLocalOnly, }); } else if (props.src === 'mentions') { connection = stream.useChannel('main'); @@ -211,6 +216,7 @@ function updatePaginationQuery() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, withReplies: props.withReplies, + withLocalOnly: props.withLocalOnly, }; } else if (props.src === 'vmimi-relay-social') { endpoint = 'notes/vmimi-relay-hybrid-timeline'; @@ -218,6 +224,7 @@ function updatePaginationQuery() { withRenotes: props.withRenotes, withFiles: props.onlyFiles ? true : undefined, withReplies: props.withReplies, + withLocalOnly: props.withLocalOnly, }; } 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 49175b2b5a8a..bba107c23327 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -17,12 +17,13 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -76,6 +77,10 @@ const withRenotes = computed({ get: () => defaultStore.reactiveState.tl.value.filter.withRenotes, set: (x) => saveTlFilter('withRenotes', x), }); +const withLocalOnly = computed({ + get: () => defaultStore.reactiveState.tl.value.filter.withLocalOnly, + set: (x) => saveTlFilter('withLocalOnly', x), +}); // computed内での無限ループを防ぐためのフラグ const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>('withReplies'); @@ -263,7 +268,11 @@ const headerActions = computed(() => { text: i18n.ts.fileAttachedOnly, ref: onlyFiles, disabled: src.value === 'local' || src.value === 'social' || src.value === 'vmimi-relay' || src.value === 'vmimi-relay-social' ? withReplies : false, - }], ev.currentTarget ?? ev.target); + }, src.value === 'vmimi-relay' || src.value === 'vmimi-relay-social' ? { + type: 'switch', + text: i18n.ts.showLocalOnlyInTimeline, + ref: withLocalOnly, + } : undefined], ev.currentTarget ?? ev.target); }, }, ]; diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index ca9f1ef93983..1c824a564723 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -200,6 +200,7 @@ export const defaultStore = markRaw(new Storage('base', { withRenotes: true, withSensitive: true, onlyFiles: false, + withLocalOnly: true, }, }, }, diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 77b4d68e0839..c9dae2e6f3fb 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -34,6 +34,7 @@ export type Column = { withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; + withLocalOnly?: boolean; soundSetting: SoundStore; }; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 751adc076d0a..17b8f7d88855 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -25,11 +25,12 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -63,6 +64,7 @@ const soundSetting = ref(props.column.soundSetting ?? { type: null, const withRenotes = ref(props.column.withRenotes ?? true); const withReplies = ref(props.column.withReplies ?? false); const onlyFiles = ref(props.column.onlyFiles ?? false); +const withLocalOnly = ref(props.column.withLocalOnly ?? true); watch(withRenotes, v => { updateColumn(props.column.id, { @@ -82,6 +84,12 @@ watch(onlyFiles, v => { }); }); +watch(withLocalOnly, v => { + updateColumn(props.column.id, { + withLocalOnly: v, + }); +}); + watch(soundSetting, v => { updateColumn(props.column.id, { soundSetting: v }); }); @@ -150,7 +158,11 @@ const menu: MenuItem[] = [{ text: i18n.ts.fileAttachedOnly, ref: onlyFiles, disabled: props.column.tl === 'local' || props.column.tl === 'social' || props.column.tl === 'vmimi-relay-social' || props.column.tl === 'vmimi-relay' ? withReplies : false, -}]; +}, props.column.tl === 'vmimi-relay-social' || props.column.tl === 'vmimi-relay' ? { + type: 'switch', + text: i18n.ts.showLocalOnlyInTimeline, + ref: withLocalOnly, +} : undefined];