diff --git a/src/client/slidingSync.ts b/src/client/slidingSync.ts index ef5ebfa98..c1923ff83 100644 --- a/src/client/slidingSync.ts +++ b/src/client/slidingSync.ts @@ -32,7 +32,6 @@ import { createDebugLogger } from '$utils/debugLogger'; import { CustomStateEvent } from '$types/matrix/room'; import * as Sentry from '@sentry/react'; import { SlidingSyncSidebarCache } from './slidingSyncSidebarCache'; -import { forwardTimelineStickyEvents, StickyEventsExtension } from './stickyEvents'; import { markPreprocessingSlidingSyncTimelineReset } from './slidingSyncTimelineReset'; const log = createLogger('slidingSync'); @@ -613,8 +612,6 @@ export class SlidingSyncManager { private hydratingSidebarCache = false; - private detachStickyEvents: (() => void) | undefined; - private readonly onCacheRoomData: (roomId: string, data: MSC3575RoomData) => void; private readonly onCacheAccountData: (event: MatrixEvent) => void; @@ -776,8 +773,6 @@ export class SlidingSyncManager { buildSpaceImagePackSubscription() ); - this.slidingSync.registerExtension(new StickyEventsExtension(mx)); - this.onLifecycle = (state, resp, err) => { debugLog.info('sync', `Sliding sync lifecycle: ${state}`, { state, @@ -999,7 +994,6 @@ export class SlidingSyncManager { this.slidingSync.on(SlidingSyncEvent.RoomData, this.onCacheRoomData); this.mx.on(RoomMemberEvent.Membership, this.onMembershipLeave); this.mx.on(ClientEvent.AccountData, this.onCacheAccountData); - this.detachStickyEvents = forwardTimelineStickyEvents(this.mx); this.armPollWatchdog(); @@ -1173,8 +1167,6 @@ export class SlidingSyncManager { this.cacheHydrationResolve = undefined; this.mx.removeListener(RoomMemberEvent.Membership, this.onMembershipLeave); this.mx.removeListener(ClientEvent.AccountData, this.onCacheAccountData); - this.detachStickyEvents?.(); - this.detachStickyEvents = undefined; this.sidebarCache.dispose(); debugLog.info('sync', 'Sliding sync disposed successfully', { diff --git a/src/client/stickyEvents.test.ts b/src/client/stickyEvents.test.ts deleted file mode 100644 index 3a6a9a098..000000000 --- a/src/client/stickyEvents.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import type { MatrixClient, Room } from '$types/matrix-sdk'; -import { MatrixEvent, RoomEvent } from '$types/matrix-sdk'; -import { forwardTimelineStickyEvents, StickyEventsExtension } from './stickyEvents'; - -const roomId = '!room:example.com'; -const userId = '@user:example.com'; - -const eventJson = (eventId: string, sticky = true) => ({ - type: 'm.rtc.member', - event_id: eventId, - sender: userId, - origin_server_ts: Date.now(), - content: { msc4354_sticky_key: `${userId}:DEVICE` }, - ...(sticky ? { msc4354_sticky: { duration_ms: 60_000 } } : {}), -}); - -const makeRoom = (existing: MatrixEvent[] = []) => { - const added: MatrixEvent[][] = []; - const room = { - roomId, - _unstable_getStickyEvents: () => existing, - _unstable_addStickyEvents: (events: MatrixEvent[]) => added.push(events), - } as unknown as Room; - return { room, added }; -}; - -const makeClient = (room: Room | undefined, supported = true) => - ({ - getRoom: () => room, - getEventMapper: () => (event: Record) => new MatrixEvent(event), - doesServerSupportUnstableFeature: vi.fn<() => Promise>().mockResolvedValue(supported), - }) as unknown as MatrixClient; - -describe('StickyEventsExtension', () => { - it('stays disabled when the server does not support MSC4354', async () => { - const extension = new StickyEventsExtension(makeClient(undefined, false)); - expect(await extension.onRequest(true)).toEqual({ enabled: false }); - }); - - it('enables itself and threads the since token', async () => { - const extension = new StickyEventsExtension(makeClient(makeRoom().room)); - - expect(await extension.onRequest(true)).toEqual({ enabled: true, limit: 100 }); - - await extension.onResponse({ next_batch: '42' }); - expect(await extension.onRequest(false)).toEqual({ enabled: true, limit: 100, since: '42' }); - - expect(await extension.onRequest(true)).toEqual({ enabled: true, limit: 100 }); - }); - - it('feeds sticky events into the room store', async () => { - const { room, added } = makeRoom(); - const extension = new StickyEventsExtension(makeClient(room)); - - await extension.onResponse({ - rooms: { [roomId]: { events: [eventJson('$one'), eventJson('$two')] } }, - }); - - expect(added).toHaveLength(1); - expect(added[0]?.map((event) => event.getId())).toEqual(['$one', '$two']); - expect(added[0]?.[0]?.getRoomId()).toBe(roomId); - }); - - it('ignores events the server did not mark sticky', async () => { - const { room, added } = makeRoom(); - const extension = new StickyEventsExtension(makeClient(room)); - - await extension.onResponse({ - rooms: { [roomId]: { events: [eventJson('$plain', false)] } }, - }); - - expect(added).toHaveLength(0); - }); - - it('does not re-add an event the store already holds', async () => { - const known = new MatrixEvent(eventJson('$one')); - const { room, added } = makeRoom([known]); - const extension = new StickyEventsExtension(makeClient(room)); - - await extension.onResponse({ - rooms: { [roomId]: { events: [eventJson('$one'), eventJson('$two')] } }, - }); - - expect(added).toHaveLength(1); - expect(added[0]?.map((event) => event.getId())).toEqual(['$two']); - }); -}); - -const makeEmitter = () => { - const listeners = new Set<(...args: unknown[]) => void>(); - const mx = { - on: (_event: string, listener: (...args: unknown[]) => void) => listeners.add(listener), - removeListener: (_event: string, listener: (...args: unknown[]) => void) => - listeners.delete(listener), - } as unknown as MatrixClient; - const emit = (...args: unknown[]) => listeners.forEach((listener) => listener(...args)); - return { mx, emit, listeners }; -}; - -describe('forwardTimelineStickyEvents', () => { - it('forwards live sticky timeline events to the room store', () => { - const { room, added } = makeRoom(); - const { mx, emit } = makeEmitter(); - forwardTimelineStickyEvents(mx); - - emit(new MatrixEvent(eventJson('$one')), room, false); - - expect(added).toHaveLength(1); - expect(added[0]?.[0]?.getId()).toBe('$one'); - }); - - it('ignores back-paginated and non-sticky events', () => { - const { room, added } = makeRoom(); - const { mx, emit } = makeEmitter(); - forwardTimelineStickyEvents(mx); - - emit(new MatrixEvent(eventJson('$one')), room, true); - emit(new MatrixEvent(eventJson('$two', false)), room, false); - - expect(added).toHaveLength(0); - }); - - it('stops forwarding once detached', () => { - const { room, added } = makeRoom(); - const { mx, emit, listeners } = makeEmitter(); - const detach = forwardTimelineStickyEvents(mx); - - detach(); - expect(listeners.size).toBe(0); - emit(new MatrixEvent(eventJson('$one')), room, false); - - expect(added).toHaveLength(0); - }); - - it('registers on the timeline event', () => { - const on = vi.fn<(event: string, listener: () => void) => void>(); - const removeListener = vi.fn<(event: string, listener: () => void) => void>(); - forwardTimelineStickyEvents({ on, removeListener } as unknown as MatrixClient); - expect(on).toHaveBeenCalledWith(RoomEvent.Timeline, expect.any(Function)); - }); -}); diff --git a/src/client/stickyEvents.ts b/src/client/stickyEvents.ts deleted file mode 100644 index d751fb922..000000000 --- a/src/client/stickyEvents.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { Extension, IRoomEvent, MatrixClient, MatrixEvent, Room } from '$types/matrix-sdk'; -import { ExtensionState, RoomEvent, UNSTABLE_MSC4354_STICKY_EVENTS } from '$types/matrix-sdk'; -import { createDebugLogger } from '$utils/debugLogger'; - -const debugLog = createDebugLogger('slidingSync'); - -const STICKY_EVENTS_EXTENSION = 'org.matrix.msc4354.sticky_events'; -const STICKY_EVENTS_LIMIT = 100; - -type StickyEventsRequest = { - enabled: boolean; - limit?: number; - since?: string; -}; - -type StickyEventsResponse = { - next_batch?: string; - rooms?: Record; -}; - -const isSticky = (event: MatrixEvent): boolean => event.unstableStickyInfo !== undefined; - -const rejectKnownStickyEvents = (room: Room, events: MatrixEvent[]): MatrixEvent[] => { - const known = new Set(); - for (const event of room._unstable_getStickyEvents()) { - const eventId = event.getId(); - if (eventId) known.add(eventId); - } - return events.filter((event) => { - const eventId = event.getId(); - return !eventId || !known.has(eventId); - }); -}; - -export const addStickyEvents = (room: Room, events: MatrixEvent[]): void => { - const sticky = rejectKnownStickyEvents(room, events.filter(isSticky)); - if (sticky.length === 0) return; - room._unstable_addStickyEvents(sticky); -}; - -export class StickyEventsExtension implements Extension { - private since: string | undefined; - - private serverSupport: Promise | undefined; - - public constructor(private readonly mx: MatrixClient) {} - - public name(): string { - return STICKY_EVENTS_EXTENSION; - } - - public when(): ExtensionState { - return ExtensionState.PostProcess; - } - - private supported(): Promise { - this.serverSupport ??= this.mx - .doesServerSupportUnstableFeature(UNSTABLE_MSC4354_STICKY_EVENTS) - .catch(() => false); - return this.serverSupport; - } - - public async onRequest(isInitial: boolean): Promise { - if (isInitial) this.since = undefined; - if (!(await this.supported())) return { enabled: false }; - return { - enabled: true, - limit: STICKY_EVENTS_LIMIT, - ...(this.since ? { since: this.since } : {}), - }; - } - - public async onResponse(data: StickyEventsResponse): Promise { - if (!data) return; - if (data.next_batch) this.since = data.next_batch; - - const mapper = this.mx.getEventMapper(); - for (const [roomId, roomData] of Object.entries(data.rooms ?? {})) { - const room = this.mx.getRoom(roomId); - if (!room) { - debugLog.warn('sync', `sticky events for unknown room ${roomId}`); - continue; - } - const events = (roomData.events ?? []).map((event) => mapper({ ...event, room_id: roomId })); - addStickyEvents(room, events); - } - } -} - -export const forwardTimelineStickyEvents = (mx: MatrixClient): (() => void) => { - const onTimeline = ( - event: MatrixEvent, - room: Room | undefined, - toStartOfTimeline: boolean | undefined - ) => { - if (!room || toStartOfTimeline || !isSticky(event)) return; - addStickyEvents(room, [event]); - }; - - mx.on(RoomEvent.Timeline, onTimeline); - return () => mx.removeListener(RoomEvent.Timeline, onTimeline); -};