From 3ff8fb8e556f4f136686dd18032d10f23cc04b03 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Fri, 24 Jul 2026 19:47:40 -0400 Subject: [PATCH] fix(export): advance the raw pagination boundary on every event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exporting a date range from an ENCRYPTED room over-paginated and mislabeled "truncated": oldestRawTs (the how-far-back-have-we-paged boundary) was updated only after the RoomMessage + decryption-failure filters, so undecryptable or non-message old events never advanced it, the fromTs break never fired, and the loop ran to MAX_EXPORT_PAGES. getTs() is unencrypted envelope metadata, so the boundary update now runs for every event, above the filters. Guarded with `ts > 0` so a bogus 0/negative origin_server_ts can't collapse the boundary and cause the opposite failure — a silent early break / under-paginated export (per review, silent omission in an export is worse than the loud over-pagination this fixes). oldestTs (oldest collected in-range message) is unchanged. Two review agents (both confirmed getTs is decryption-independent, no intra-page collection regression, oldestRawTs feeds only the fromTs break, no plaintext regression); the second surfaced the 0-ts under-pagination edge, hence the guard. Not unit-testable (embedded component + needs an E2EE room with undecryptable history). Gate-green (tsc, eslint, prettier, 925 tests, build). Co-Authored-By: Claude Opus 4.8 --- src/app/features/room-settings/ExportRoomHistory.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app/features/room-settings/ExportRoomHistory.tsx b/src/app/features/room-settings/ExportRoomHistory.tsx index 83596c6ee..da3a295c7 100644 --- a/src/app/features/room-settings/ExportRoomHistory.tsx +++ b/src/app/features/room-settings/ExportRoomHistory.tsx @@ -92,6 +92,16 @@ export function ExportRoomHistory({ requestClose }: ExportRoomHistoryProps) { const evId = ev.getId(); if (!evId || seen.has(evId)) continue; seen.add(evId); + // Advance the raw pagination boundary for EVERY event (any type, + // decrypted or not) — getTs() is unencrypted metadata. Gating this on + // a decrypted m.room.message let undecryptable/non-message old events + // stall oldestRawTs, so the fromTs break never fired → over-paginate + // and a false "truncated". + const ts = ev.getTs(); + // Require a positive ts: an event with a bogus 0/negative + // origin_server_ts must not collapse the boundary and trigger an early + // break (silent under-pagination in the export). + if (ts > 0 && ts < oldestRawTs) oldestRawTs = ts; // Attempt decryption for events that haven't been decrypted yet // (paginateEventTimeline may fetch events before the SDK decrypts them) if (ev.isEncrypted() && !ev.getClearContent()) { @@ -100,8 +110,6 @@ export function ExportRoomHistory({ requestClose }: ExportRoomHistoryProps) { } if (ev.getType() !== EventType.RoomMessage) continue; if (ev.isDecryptionFailure()) continue; - const ts = ev.getTs(); - if (ts < oldestRawTs) oldestRawTs = ts; if (fromTs !== null && ts < fromTs) continue; if (toTs !== null && ts > toTs) continue; const content = ev.getContent();