From 1ea37d0ec125c5d9820b9d52cdb1a4239ffbb7f7 Mon Sep 17 00:00:00 2001 From: jamie Date: Wed, 29 Apr 2026 21:55:49 -0400 Subject: [PATCH 01/84] feat: add useAlive composable --- app/composables/use-alive.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/composables/use-alive.ts diff --git a/app/composables/use-alive.ts b/app/composables/use-alive.ts new file mode 100644 index 00000000..2e79c1d1 --- /dev/null +++ b/app/composables/use-alive.ts @@ -0,0 +1,11 @@ +/** + * @returns boolean depicting if component is currently alive. Useful for components wrapped in + */ +export function useAlive() { + const isAlive = shallowRef(true) + + onActivated(() => isAlive.value = true) + onDeactivated(() => isAlive.value = false) + + return isAlive +} From ccce857459e9b5f67be76c40567ea4a585fb7ab9 Mon Sep 17 00:00:00 2001 From: jamie Date: Wed, 29 Apr 2026 22:01:52 -0400 Subject: [PATCH 02/84] fix: prevent pagination spam in useEventPagination when using samew instance --- app/composables/use-event-pagination.ts | 32 ++++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/app/composables/use-event-pagination.ts b/app/composables/use-event-pagination.ts index a7079fc2..e0d2ac07 100644 --- a/app/composables/use-event-pagination.ts +++ b/app/composables/use-event-pagination.ts @@ -138,9 +138,13 @@ export function useEventPagination(opts: Opts) { const paginateMutex = new Mutex() async function paginate(dir: Direction) { await paginateMutex.acquire() + const startedForRoomId = opts.room.value.roomId isPaginating.value = true + let nextDir: Direction | null = null try { + if (opts.room.value.roomId !== startedForRoomId) + return if (dir === Direction.Backward && !canPaginateBackward.value) return if (dir === Direction.Forward && !canPaginateForward.value) @@ -151,6 +155,8 @@ export function useEventPagination(opts: Opts) { return const sentinels = await getNextPageSentinels(dir) + if (opts.room.value.roomId !== startedForRoomId) + return if (!sentinels) return @@ -159,29 +165,37 @@ export function useEventPagination(opts: Opts) { await setRange({ dir }) await nextTick() - } - finally { - const container = unrefElement(scrollEl) - if (container) { + if (opts.room.value.roomId !== startedForRoomId) + return + + const containerAfter = unrefElement(scrollEl) + if (containerAfter) { const backwardEl = backwardSentinelEl.value const forwardEl = forwardSentinelEl.value - const backwardIntersecting = dir !== Direction.Backward && backwardEl && isIntersecting(container, backwardEl) - const forwardIntersecting = dir !== Direction.Forward && forwardEl && isIntersecting(container, forwardEl) + const backwardIntersecting = dir !== Direction.Backward && backwardEl && isIntersecting(containerAfter, backwardEl) + const forwardIntersecting = dir !== Direction.Forward && forwardEl && isIntersecting(containerAfter, forwardEl) if (backwardIntersecting) - void paginate(Direction.Backward) + nextDir = Direction.Backward else if (forwardIntersecting) - void paginate(Direction.Forward) + nextDir = Direction.Forward } - + } + finally { isPaginating.value = false paginateMutex.release() + if (nextDir !== null && opts.room.value.roomId === startedForRoomId) + await paginate(nextDir) } } async function handleOnMounted() { + eventsPaginated.value = events.value.slice(-80) + + await nextTick() + const anchor = getAnchor(Direction.Backward, maxPageHeight.value * 0.75) const cachedScrollState = pageScrollStateCache.get(opts.room.value.roomId) const container = unrefElement(scrollEl) From 2941f06fe913dddf440b42eb0625a529ac98843c Mon Sep 17 00:00:00 2001 From: jamie Date: Wed, 29 Apr 2026 22:08:10 -0400 Subject: [PATCH 03/84] refactor: make isFullyLoaded flag in useRoomEvents reactive to room --- app/composables/use-room-events.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/app/composables/use-room-events.ts b/app/composables/use-room-events.ts index 195a44ab..0f7fdcce 100644 --- a/app/composables/use-room-events.ts +++ b/app/composables/use-room-events.ts @@ -5,13 +5,23 @@ export const BATCH_SIZE = 80 type Hooks = Prettify[1]>>, 'onTimelineRefresh' | 'onTimeline' | 'onTimelineReset'>> +const roomEventsFullyLoadedSet = new Set() + export function useRoomEvents(room: Ref, hooks?: Partial) { const { client } = useMatrixClient() const events = shallowRef([]) const eventVersions = shallowReactive(new Map()) - const isFullyLoaded = useState(`${room.value.roomId}:isFullyLoaded`, () => false) + const isFullyLoaded = computed({ + get: () => roomEventsFullyLoadedSet.has(room.value.roomId), + set: (v: boolean) => { + if (v) + roomEventsFullyLoadedSet.add(room.value.roomId) + else + roomEventsFullyLoadedSet.delete(room.value.roomId) + }, + }) const sync = () => { const liveEvents = room.value.getLiveTimeline().getEvents() @@ -19,7 +29,7 @@ export function useRoomEvents(room: Ref, hooks?: Partial) { events.value = [...(liveEvents ?? [])] } - whenever(room, sync, { immediate: true, once: true }) + whenever(room, sync, { immediate: true }) let currentBatchSize = BATCH_SIZE const mutex = new Mutex() @@ -34,6 +44,8 @@ export function useRoomEvents(room: Ref, hooks?: Partial) { if (!r) return + const targetRoomId = r.roomId + if (dir === Direction.Backward) { const canLoadMore = await retry( scrollBack, @@ -47,10 +59,12 @@ export function useRoomEvents(room: Ref, hooks?: Partial) { }, ) - isFullyLoaded.value = !canLoadMore + if (targetRoomId === toValue(room).roomId) + isFullyLoaded.value = !canLoadMore } - sync() + if (targetRoomId === toValue(room).roomId) + sync() } finally { mutex.release() From 5910e65f506cebb8d91209bfe3842d19bc423bb5 Mon Sep 17 00:00:00 2001 From: jamie Date: Wed, 29 Apr 2026 22:08:51 -0400 Subject: [PATCH 04/84] feat: use useAlive inside layout app slot to prevent persistent renders --- app/components/layout/app/slot.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/components/layout/app/slot.vue b/app/components/layout/app/slot.vue index 184bee4b..5cdd7e26 100644 --- a/app/components/layout/app/slot.vue +++ b/app/components/layout/app/slot.vue @@ -10,10 +10,12 @@ const props = defineProps<{ }>() const to = computed(() => `#app-${props.name}`) + +const isAlive = useAlive() From d34de418e099317c5f98b68f0626e2e9c897152d Mon Sep 17 00:00:00 2001 From: jamie Date: Wed, 29 Apr 2026 22:13:33 -0400 Subject: [PATCH 05/84] refactor: move page logic to space route + add comment in room page --- app/pages/app/space/[spaceId].vue | 24 ++++++++++++++++++- app/pages/app/space/[spaceId]/[roomId].vue | 27 +++------------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/app/pages/app/space/[spaceId].vue b/app/pages/app/space/[spaceId].vue index 29cd11c8..6a2cbe78 100644 --- a/app/pages/app/space/[spaceId].vue +++ b/app/pages/app/space/[spaceId].vue @@ -7,6 +7,9 @@ definePageMeta({ const currentSpace = useCurrentSpace() const joinedRooms = useJoinedRooms(() => currentSpace.value?.roomId) + +const currentRoom = useCurrentRoom() +const isPaginating = shallowRef(false) diff --git a/app/pages/app/space/[spaceId]/[roomId].vue b/app/pages/app/space/[spaceId]/[roomId].vue index b2e7b3aa..28e4c1de 100644 --- a/app/pages/app/space/[spaceId]/[roomId].vue +++ b/app/pages/app/space/[spaceId]/[roomId].vue @@ -4,28 +4,7 @@ definePageMeta({ name: 'space-room', }) -const currentRoom = useCurrentRoom() -const currentSpace = useCurrentSpace() - -const isPaginating = shallowRef(false) +// this page is here solely to allow for navigation to it. the actual logic for the page is +// inside the `/pages/app/space/[spaceId].vue` page. the reason for this is to persist the +// event list component instance across route changes - - From bddb244f5e0c68c41a4857859cc107bf5979fd64 Mon Sep 17 00:00:00 2001 From: jamie Date: Wed, 29 Apr 2026 22:13:46 -0400 Subject: [PATCH 06/84] fix: run handleOnMounted in event list component on room change --- app/components/page/room/event-list.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/components/page/room/event-list.vue b/app/components/page/room/event-list.vue index 5eb170d4..7fe53175 100644 --- a/app/components/page/room/event-list.vue +++ b/app/components/page/room/event-list.vue @@ -32,6 +32,9 @@ onMounted(async () => { await handleOnMounted() }) +// does not fire on mount (no immediate: true) +watch(() => props.room.roomId, handleOnMounted) + watch(isPaginating, v => emits('isPaginating', v)) const groupedEvents = useEventGrouping({ events, eventsPaginated }) From 5c91b8dc712fc1297e9298cd4a03663b8421ee00 Mon Sep 17 00:00:00 2001 From: jamie Date: Wed, 29 Apr 2026 22:19:31 -0400 Subject: [PATCH 07/84] fix: fix type error --- app/utils/matrix/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/utils/matrix/types.ts b/app/utils/matrix/types.ts index 86e33491..2b99390c 100644 --- a/app/utils/matrix/types.ts +++ b/app/utils/matrix/types.ts @@ -1,4 +1,5 @@ import type { ClientEventHandlerMap, EmittedEvents, EventEmitterEvents, Listener, Room, User } from 'matrix-js-sdk' +import type { getPowerLevelName } from './room' export type EmitterListener = Listener From 1fa1d3d0639ec775a800f4af8d046a16dfb606df Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 30 Apr 2026 13:05:14 -0400 Subject: [PATCH 08/84] fix: remove redundant v-show --- app/pages/app/space/[spaceId].vue | 1 - 1 file changed, 1 deletion(-) diff --git a/app/pages/app/space/[spaceId].vue b/app/pages/app/space/[spaceId].vue index 6a2cbe78..31907f53 100644 --- a/app/pages/app/space/[spaceId].vue +++ b/app/pages/app/space/[spaceId].vue @@ -93,7 +93,6 @@ const isPaginating = shallowRef(false)
From e6188f5a99f48aa02f0511422a327d16be01b56f Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 30 Apr 2026 13:06:42 -0400 Subject: [PATCH 09/84] refactor: remove redundant guard --- app/composables/use-event-pagination.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/composables/use-event-pagination.ts b/app/composables/use-event-pagination.ts index e0d2ac07..a40e6f1b 100644 --- a/app/composables/use-event-pagination.ts +++ b/app/composables/use-event-pagination.ts @@ -143,8 +143,6 @@ export function useEventPagination(opts: Opts) { let nextDir: Direction | null = null try { - if (opts.room.value.roomId !== startedForRoomId) - return if (dir === Direction.Backward && !canPaginateBackward.value) return if (dir === Direction.Forward && !canPaginateForward.value) From 18f17c1da963dd69eee2f3f876ed395fcee8d95b Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 30 Apr 2026 13:07:56 -0400 Subject: [PATCH 10/84] fix: restore removed defer --- app/components/layout/app/slot.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/components/layout/app/slot.vue b/app/components/layout/app/slot.vue index 5cdd7e26..34b811f0 100644 --- a/app/components/layout/app/slot.vue +++ b/app/components/layout/app/slot.vue @@ -15,7 +15,11 @@ const isAlive = useAlive() From 07f981c7d9f4dc04383b971d5620391c6eac7d9e Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 30 Apr 2026 13:09:00 -0400 Subject: [PATCH 11/84] fix: wrap event list + input inside a v-for template block to prevent input from rendering on non-room routes --- app/pages/app/space/[spaceId].vue | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/pages/app/space/[spaceId].vue b/app/pages/app/space/[spaceId].vue index 31907f53..f9648d32 100644 --- a/app/pages/app/space/[spaceId].vue +++ b/app/pages/app/space/[spaceId].vue @@ -91,12 +91,13 @@ const isPaginating = shallowRef(false)
- - +
From dcc32bed2ece8d99350cb62dd5c98bff7117e277 Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 30 Apr 2026 13:25:20 -0400 Subject: [PATCH 12/84] fix: consolidate mount logic + use it for room update --- app/components/page/room/event-list.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/components/page/room/event-list.vue b/app/components/page/room/event-list.vue index 7fe53175..284bb8fd 100644 --- a/app/components/page/room/event-list.vue +++ b/app/components/page/room/event-list.vue @@ -26,14 +26,14 @@ const { scrollEl: containerRef, }) -onMounted(async () => { +onMounted(handleRoomUpdate) +watch(() => props.room.roomId, handleRoomUpdate) + +async function handleRoomUpdate() { await nextTick() scrollToBottom() await handleOnMounted() -}) - -// does not fire on mount (no immediate: true) -watch(() => props.room.roomId, handleOnMounted) +} watch(isPaginating, v => emits('isPaginating', v)) From 5b30bb4e5ed746f7e8223d4ff8509ed2503f4f91 Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 30 Apr 2026 13:43:56 -0400 Subject: [PATCH 13/84] fix: retain reactivity for roomEventsFullyLoadedSet by wrapping it in reactive() --- app/composables/use-room-events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/composables/use-room-events.ts b/app/composables/use-room-events.ts index 0f7fdcce..7d829dc9 100644 --- a/app/composables/use-room-events.ts +++ b/app/composables/use-room-events.ts @@ -5,7 +5,7 @@ export const BATCH_SIZE = 80 type Hooks = Prettify[1]>>, 'onTimelineRefresh' | 'onTimeline' | 'onTimelineReset'>> -const roomEventsFullyLoadedSet = new Set() +const roomEventsFullyLoadedSet = reactive(new Set()) export function useRoomEvents(room: Ref, hooks?: Partial) { const { client } = useMatrixClient() From 91ff27fdebe0a1b83b9975353ae02326f5ee3f41 Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 30 Apr 2026 13:57:49 -0400 Subject: [PATCH 14/84] fix: fix type errors --- .nuxtrc | 2 +- app/utils/matrix/types.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.nuxtrc b/.nuxtrc index 4f03c3fc..640f280a 100644 --- a/.nuxtrc +++ b/.nuxtrc @@ -1 +1 @@ -setups.@nuxt/test-utils="4.0.2" \ No newline at end of file +setups.@nuxt/test-utils="4.0.3" \ No newline at end of file diff --git a/app/utils/matrix/types.ts b/app/utils/matrix/types.ts index 2b99390c..86e33491 100644 --- a/app/utils/matrix/types.ts +++ b/app/utils/matrix/types.ts @@ -1,5 +1,4 @@ import type { ClientEventHandlerMap, EmittedEvents, EventEmitterEvents, Listener, Room, User } from 'matrix-js-sdk' -import type { getPowerLevelName } from './room' export type EmitterListener = Listener From cb4ba1af87dea3f97db965592e46b8d9326fbcb3 Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 30 Apr 2026 13:58:38 -0400 Subject: [PATCH 15/84] fix: fix potential concurrency bug in event list --- app/components/page/room/event-list.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/components/page/room/event-list.vue b/app/components/page/room/event-list.vue index 284bb8fd..5897ce56 100644 --- a/app/components/page/room/event-list.vue +++ b/app/components/page/room/event-list.vue @@ -30,7 +30,12 @@ onMounted(handleRoomUpdate) watch(() => props.room.roomId, handleRoomUpdate) async function handleRoomUpdate() { + const expectedRoomId = props.room.roomId await nextTick() + + if (props.room.roomId !== expectedRoomId) + return + scrollToBottom() await handleOnMounted() } From 86fa62be2bb8843f85020d09a9d7104904a968af Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 30 Apr 2026 14:19:13 -0400 Subject: [PATCH 16/84] fix: fix bug with stale sentinel state when swapping rooms --- app/composables/use-event-pagination.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/composables/use-event-pagination.ts b/app/composables/use-event-pagination.ts index a40e6f1b..1924398f 100644 --- a/app/composables/use-event-pagination.ts +++ b/app/composables/use-event-pagination.ts @@ -72,6 +72,10 @@ export function useEventPagination(opts: Opts) { if (isPaginating.value) return + const prevLastId = prevEvents?.at(-1)?.getId() + if (prevLastId && !newEvents.some(e => e.getId() === prevLastId)) + return + const container = unrefElement(scrollEl) if (!container) return From e5b8f3d3ba821c62c79f250af26eddf1c7744d7d Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 30 Apr 2026 14:48:09 -0400 Subject: [PATCH 17/84] fix: fix invalid room id acquire before mutex acquire in useEventPagination --- app/composables/use-event-pagination.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/composables/use-event-pagination.ts b/app/composables/use-event-pagination.ts index 1924398f..8c95bc2c 100644 --- a/app/composables/use-event-pagination.ts +++ b/app/composables/use-event-pagination.ts @@ -141,8 +141,8 @@ export function useEventPagination(opts: Opts) { const paginateMutex = new Mutex() async function paginate(dir: Direction) { - await paginateMutex.acquire() const startedForRoomId = opts.room.value.roomId + await paginateMutex.acquire() isPaginating.value = true let nextDir: Direction | null = null From 4c7c7dfebea94da2f5c2d41df252cb4e53679b68 Mon Sep 17 00:00:00 2001 From: jamie Date: Thu, 30 Apr 2026 15:45:48 -0400 Subject: [PATCH 18/84] feat: add environment-specific plugin that displays page load time --- app/components/layout/app/header-text.vue | 2 +- app/components/layout/app/header.vue | 8 ++++-- app/plugins/load-time.client.ts | 33 +++++++++++++++++++++++ nuxt.config.ts | 4 +++ 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 app/plugins/load-time.client.ts diff --git a/app/components/layout/app/header-text.vue b/app/components/layout/app/header-text.vue index bc4f6eb0..a525e2d3 100644 --- a/app/components/layout/app/header-text.vue +++ b/app/components/layout/app/header-text.vue @@ -21,7 +21,7 @@ const motionProps: MotionProps = {