Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/feat-background-pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Add adaptive background pagination for improved message history availability.
32 changes: 31 additions & 1 deletion src/app/features/room/Room.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,6 +16,7 @@
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';
Expand Down Expand Up @@ -45,6 +47,34 @@
)
);

// 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);

Check warning on line 67 in src/app/features/room/Room.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected console statement
});
}
}, config.delayMs);

return () => {
cancelled = true;
clearTimeout(timer);
};
}, [room, mx]);

const callView = room.isCallRoom();

return (
Expand Down
119 changes: 119 additions & 0 deletions src/app/utils/device-capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* 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;
}

/**
* Adaptive signals from browser APIs for device/network capability detection.
* Shared by sliding sync and background pagination for consistent behavior.
*/
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;
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,
};
}
}
34 changes: 1 addition & 33 deletions src/client/slidingSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -86,39 +87,6 @@ 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,
};
};

// 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 = (
Expand Down
Loading