From d4ec82e0aafb53729543f2ae8414a83c09f2dc4e Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Tue, 10 Mar 2026 08:44:09 -0400 Subject: [PATCH 1/4] feat: Add adaptive background pagination for message history - Add device capability detection using same logic as sliding sync - Implement background pagination on room open with adaptive limits: - High-end (4g+/desktop/high-memory): 500 messages, 1s delay - Medium (3g or <=4GB RAM): 250 messages, 2s delay - Low-end (save-data/2g or mobile): 100 messages, 3s delay - Detection based on: connection type, device memory, save-data preference - Improves message history availability without blocking initial room load - Consistent with sliding sync's adaptive behavior --- .changeset/feat-background-pagination.md | 5 ++ src/app/features/room/Room.tsx | 32 ++++++- src/app/utils/device-capabilities.ts | 107 +++++++++++++++++++++++ 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 .changeset/feat-background-pagination.md create mode 100644 src/app/utils/device-capabilities.ts diff --git a/.changeset/feat-background-pagination.md b/.changeset/feat-background-pagination.md new file mode 100644 index 000000000..73df75303 --- /dev/null +++ b/.changeset/feat-background-pagination.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add adaptive background pagination for improved message history availability. When entering a room, the app now proactively loads additional message history in the background based on device capabilities (high-end: 500 messages, medium: 250 messages, low-end/mobile: 100 messages) without blocking the initial room load. Uses the same adaptive detection logic as sliding sync for consistency. diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx index b7aef9107..1abfd9399 100644 --- a/src/app/features/room/Room.tsx +++ b/src/app/features/room/Room.tsx @@ -1,8 +1,9 @@ -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import { Box, Line } from 'folds'; import { useParams } from 'react-router-dom'; import { isKeyHotkey } from 'is-hotkey'; import { useAtomValue } from 'jotai'; +import { Direction } from '$types/matrix-sdk'; import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; import { useSetting } from '$state/hooks/settings'; import { settingsAtom } from '$state/settings'; @@ -15,6 +16,7 @@ import { useRoomMembers } from '$hooks/useRoomMembers'; import { CallView } from '$features/call/CallView'; import { WidgetsDrawer } from '$features/widgets/WidgetsDrawer'; import { callChatAtom } from '$state/callEmbed'; +import { getBackgroundPaginationConfig } from '$utils/device-capabilities'; import { RoomViewHeader } from './RoomViewHeader'; import { MembersDrawer } from './MembersDrawer'; import { RoomView } from './RoomView'; @@ -45,6 +47,34 @@ export function Room() { ) ); + // Background pagination to load additional message history + useEffect(() => { + const config = getBackgroundPaginationConfig(); + if (!config.enabled) return undefined; + + let cancelled = false; + const timer = setTimeout(() => { + if (cancelled) return; + + const timeline = room.getLiveTimeline(); + const token = timeline.getPaginationToken(Direction.Backward); + + if (token) { + mx.paginateEventTimeline(timeline, { + backwards: true, + limit: config.limit, + }).catch((err) => { + console.warn('Background pagination failed:', err); + }); + } + }, config.delayMs); + + return () => { + cancelled = true; + clearTimeout(timer); + }; + }, [room, mx]); + const callView = room.isCallRoom(); return ( diff --git a/src/app/utils/device-capabilities.ts b/src/app/utils/device-capabilities.ts new file mode 100644 index 000000000..4c0df0b0a --- /dev/null +++ b/src/app/utils/device-capabilities.ts @@ -0,0 +1,107 @@ +/** + * Device performance tier for adaptive features + */ +export type DevicePerformanceTier = 'low' | 'medium' | 'high'; + +/** + * Configuration for background pagination based on device capabilities + */ +export interface BackgroundPaginationConfig { + enabled: boolean; + delayMs: number; + limit: number; +} + +/** + * Read adaptive signals from browser APIs + * This mirrors the logic used in slidingSync.ts for consistency + */ +function readAdaptiveSignals() { + const navigatorLike = typeof navigator !== 'undefined' ? navigator : undefined; + const connection = (navigatorLike as any)?.connection; + const effectiveType = connection?.effectiveType; + const deviceMemory = (navigatorLike as any)?.deviceMemory; + const uaMobile = (navigatorLike as any)?.userAgentData?.mobile; + const fallbackMobileUA = navigatorLike?.userAgent ?? ''; + const mobileByUA = + typeof uaMobile === 'boolean' + ? uaMobile + : /Mobi|Android|iPhone|iPad|iPod|IEMobile|Opera Mini/i.test(fallbackMobileUA); + const saveData = connection?.saveData === true; + const normalizedEffectiveType = typeof effectiveType === 'string' ? effectiveType : null; + const normalizedDeviceMemory = typeof deviceMemory === 'number' ? deviceMemory : null; + const missingSignals = + Number(normalizedEffectiveType === null) + Number(normalizedDeviceMemory === null); + + return { + saveData, + effectiveType: normalizedEffectiveType, + deviceMemoryGb: normalizedDeviceMemory, + mobile: mobileByUA, + missingSignals, + }; +} + +/** + * Detect device performance tier based on hardware capabilities + * Uses the same logic as sliding sync for consistency + */ +export function getDevicePerformanceTier(): DevicePerformanceTier { + const signals = readAdaptiveSignals(); + + // Low-end: save data enabled or very slow connection + if (signals.saveData || signals.effectiveType === 'slow-2g' || signals.effectiveType === '2g') { + return 'low'; + } + + // Medium: 3g connection or low memory device + if ( + signals.effectiveType === '3g' || + (signals.deviceMemoryGb !== null && signals.deviceMemoryGb <= 4) + ) { + return 'medium'; + } + + // Medium fallback: mobile with missing signal data + if (signals.mobile && signals.missingSignals > 0) { + return 'medium'; + } + + // High-end: everything else (4g+, desktop, or high memory) + return 'high'; +} + +/** + * Get background pagination configuration based on device capabilities + * Uses the same adaptive detection logic as sliding sync for consistency + */ +export function getBackgroundPaginationConfig(): BackgroundPaginationConfig { + const tier = getDevicePerformanceTier(); + + switch (tier) { + case 'high': + return { + enabled: true, + delayMs: 1000, // 1 second delay + limit: 500, // Load 500 messages + }; + case 'medium': + return { + enabled: true, + delayMs: 2000, // 2 second delay + limit: 250, // Load 250 messages + }; + case 'low': + return { + enabled: true, + delayMs: 3000, // 3 second delay + limit: 100, // Load 100 messages + }; + default: + return { + enabled: false, + delayMs: 0, + limit: 0, + }; + } +} From 895c292fdecd6d95a5d7772f59283a6e824f9793 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Tue, 10 Mar 2026 09:21:57 -0400 Subject: [PATCH 2/4] Update changeset --- .changeset/feat-background-pagination.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/feat-background-pagination.md b/.changeset/feat-background-pagination.md index 73df75303..9b93320ba 100644 --- a/.changeset/feat-background-pagination.md +++ b/.changeset/feat-background-pagination.md @@ -2,4 +2,4 @@ default: minor --- -Add adaptive background pagination for improved message history availability. When entering a room, the app now proactively loads additional message history in the background based on device capabilities (high-end: 500 messages, medium: 250 messages, low-end/mobile: 100 messages) without blocking the initial room load. Uses the same adaptive detection logic as sliding sync for consistency. +Add adaptive background pagination for improved message history availability. From c1f9f92acf0f88198f25bad8463542f23c0f2663 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Tue, 10 Mar 2026 09:25:24 -0400 Subject: [PATCH 3/4] refactor: Extract adaptive signal detection into shared utility - Export AdaptiveSignals type and readAdaptiveSignals() from device-capabilities.ts - Update slidingSync.ts to import shared logic instead of duplicating it - Single source of truth for device capability detection across the app - Reduces code duplication and ensures consistent behavior --- src/app/utils/device-capabilities.ts | 18 ++++++++++++--- src/client/slidingSync.ts | 34 ++-------------------------- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/src/app/utils/device-capabilities.ts b/src/app/utils/device-capabilities.ts index 4c0df0b0a..4f0dbc35d 100644 --- a/src/app/utils/device-capabilities.ts +++ b/src/app/utils/device-capabilities.ts @@ -13,10 +13,22 @@ export interface BackgroundPaginationConfig { } /** - * Read adaptive signals from browser APIs - * This mirrors the logic used in slidingSync.ts for consistency + * Adaptive signals from browser APIs for device/network capability detection. + * Shared by sliding sync and background pagination for consistent behavior. */ -function readAdaptiveSignals() { +export type AdaptiveSignals = { + saveData: boolean; + effectiveType: string | null; + deviceMemoryGb: number | null; + mobile: boolean; + missingSignals: number; +}; + +/** + * Read adaptive signals from browser APIs. + * Single source of truth for device capability detection across the app. + */ +export function readAdaptiveSignals(): AdaptiveSignals { const navigatorLike = typeof navigator !== 'undefined' ? navigator : undefined; const connection = (navigatorLike as any)?.connection; const effectiveType = connection?.effectiveType; diff --git a/src/client/slidingSync.ts b/src/client/slidingSync.ts index f2c9257dc..34b73d453 100644 --- a/src/client/slidingSync.ts +++ b/src/client/slidingSync.ts @@ -86,38 +86,8 @@ const clampPositive = (value: number | undefined, fallback: number): number => { return Math.round(value); }; -type AdaptiveSignals = { - saveData: boolean; - effectiveType: string | null; - deviceMemoryGb: number | null; - mobile: boolean; - missingSignals: number; -}; - -const readAdaptiveSignals = (): AdaptiveSignals => { - const navigatorLike = typeof navigator !== 'undefined' ? navigator : undefined; - const connection = (navigatorLike as any)?.connection; - const effectiveType = connection?.effectiveType; - const deviceMemory = (navigatorLike as any)?.deviceMemory; - const uaMobile = (navigatorLike as any)?.userAgentData?.mobile; - const fallbackMobileUA = navigatorLike?.userAgent ?? ''; - const mobileByUA = - typeof uaMobile === 'boolean' - ? uaMobile - : /Mobi|Android|iPhone|iPad|iPod|IEMobile|Opera Mini/i.test(fallbackMobileUA); - const saveData = connection?.saveData === true; - const normalizedEffectiveType = typeof effectiveType === 'string' ? effectiveType : null; - const normalizedDeviceMemory = typeof deviceMemory === 'number' ? deviceMemory : null; - const missingSignals = - Number(normalizedEffectiveType === null) + Number(normalizedDeviceMemory === null); - return { - saveData, - effectiveType: normalizedEffectiveType, - deviceMemoryGb: normalizedDeviceMemory, - mobile: mobileByUA, - missingSignals, - }; -}; +// Import shared device capability detection logic +import { readAdaptiveSignals, type AdaptiveSignals } from '../app/utils/device-capabilities'; // Resolve the timeline limit for the active-room subscription based on device/network. // The list subscription always uses LIST_TIMELINE_LIMIT=1 regardless of conditions. From 54c324d0cbf587c0da90553f6f202f5886ce3a10 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Tue, 10 Mar 2026 09:33:20 -0400 Subject: [PATCH 4/4] fix: Move import to top of file - Move readAdaptiveSignals import to top with other imports - Fixes unconventional import placement from previous refactor --- src/client/slidingSync.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client/slidingSync.ts b/src/client/slidingSync.ts index 34b73d453..c927eb559 100644 --- a/src/client/slidingSync.ts +++ b/src/client/slidingSync.ts @@ -16,6 +16,7 @@ import { User, } from '$types/matrix-sdk'; import { createLogger } from '$utils/debug'; +import { readAdaptiveSignals, type AdaptiveSignals } from '../app/utils/device-capabilities'; const log = createLogger('slidingSync'); @@ -86,9 +87,6 @@ const clampPositive = (value: number | undefined, fallback: number): number => { return Math.round(value); }; -// Import shared device capability detection logic -import { readAdaptiveSignals, type AdaptiveSignals } from '../app/utils/device-capabilities'; - // Resolve the timeline limit for the active-room subscription based on device/network. // The list subscription always uses LIST_TIMELINE_LIMIT=1 regardless of conditions. const resolveAdaptiveRoomTimelineLimit = (