Skip to content
Open
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/fix-sliding-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Improved sliding sync (only requests specific room state data), cleaned up and fixed most flickering issues, and added buttons in developer tools to request full state.
2 changes: 1 addition & 1 deletion src/app/components/sidebar/Sidebar.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export const SidebarItemBottom = recipe({

selectors: {
'&:hover': {
transform: `translateY(${toRem(PUSH_Y)})`,
transform: `translateY(${toRem(-PUSH_Y)})`,
},
'&::before': {
content: '',
Expand Down
90 changes: 52 additions & 38 deletions src/app/features/common-settings/developer-tools/DevelopTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
Plus,
X,
} from '$components/icons/phosphor';
import { EventType, NotificationCountType } from '$types/matrix-sdk';
import { EventType, NotificationCountType, type MatrixEvent } from '$types/matrix-sdk';
import { Page, PageContent, PageHeader } from '$components/page';
import { SequenceCard } from '$components/sequence-card';
import { SettingTile } from '$components/setting-tile';
Expand All @@ -20,6 +20,7 @@ import { settingsAtom } from '$state/settings';
import { copyToClipboard } from '$utils/dom';
import { getClientSyncDiagnostics } from '$client/initMatrix';
import { useRoom } from '$hooks/useRoom';
import type { StateTypeToState } from '$hooks/useRoomState';
import { useRoomState } from '$hooks/useRoomState';
import { useRoomAccountData } from '$hooks/useRoomAccountData';
import { roomToUnreadAtom } from '$state/room/roomToUnread';
Expand All @@ -35,25 +36,47 @@ import { SendRoomEvent } from './SendRoomEvent';
import type { StateEventInfo } from './StateEventEditor';
import { StateEventEditor } from './StateEventEditor';

const formatSyncReason = (reason: string): string => {
if (reason === 'sliding_active') return 'Sliding Sync active';
if (reason === 'sliding_disabled_server') return 'Server-side sliding sync disabled';
if (reason === 'session_opt_out') return 'Session opt-in is off';
if (reason === 'missing_proxy') return 'Sliding proxy URL missing';
if (reason === 'cold_cache_bootstrap') return 'Cold-cache bootstrap (classic for this run)';
if (reason === 'probe_failed_fallback') return 'Sliding probe failed, using fallback';
return reason;
};

type DeveloperToolsProps = {
requestClose: () => void;
};
export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
const [developerTools, setDeveloperTools] = useSetting(settingsAtom, 'developerTools');
const mx = useMatrixClient();
const room = useRoom();
const roomState = useRoomState(room);
const roomStateMemory = useRoomState(room);
const accountData = useRoomAccountData(room);
const [fullApiState, setFullApiState] = useState<StateTypeToState>();
const [fetchingApiState, setFetchingApiState] = useState(false);
const roomState = fullApiState ?? roomStateMemory;

const handleFetchFullState = useCallback(async () => {
setFetchingApiState(true);
try {
const stateEvents = await mx.roomState(room.roomId);
const stateMap = new Map();
for (const event of stateEvents) {
if (event.type === 'm.room.member') continue;
let kToE = stateMap.get(event.type);
if (!kToE) {
kToE = new Map();
stateMap.set(event.type, kToE);
}
// Mock MatrixEvent structure enough for UI
kToE.set(event.state_key ?? '', {
event,
getType: () => event.type,
getContent: () => event.content,
getStateKey: () => event.state_key ?? '',
getSender: () => event.sender,
} as unknown as MatrixEvent);
}
setFullApiState(stateMap);
} catch (e) {
console.error('Failed to fetch full room state:', e);
} finally {
setFetchingApiState(false);
}
}, [mx, room.roomId]);

const [expandState, setExpandState] = useState(false);
const [expandUnreadDiagnostics, setExpandUnreadDiagnostics] = useState(false);
Expand Down Expand Up @@ -391,29 +414,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
</Box>
{expandSlidingDiagnostics && (
<Box direction="Column" gap="100">
<Text size="T200">
Transport: {syncDiagnostics.transport}
{syncDiagnostics.fallbackFromSliding ? ' (fallback)' : ''}
</Text>
<Text size="T200">
Sliding configured:{' '}
{syncDiagnostics.slidingConfigured ? 'yes' : 'no'}
</Text>
<Text size="T200">
Sliding server-enabled:{' '}
{syncDiagnostics.slidingEnabledOnServer ? 'yes' : 'no'}
</Text>
<Text size="T200">
Sliding session opt-in:{' '}
{syncDiagnostics.sessionOptIn ? 'yes' : 'no'}
</Text>
<Text size="T200">
Sliding requested:{' '}
{syncDiagnostics.slidingRequested ? 'yes' : 'no'}
</Text>
<Text size="T200">
Sync reason: {formatSyncReason(syncDiagnostics.reason)}
</Text>
<Text size="T200">Transport: {syncDiagnostics.transport}</Text>
<Text size="T200">
Client sync state: {syncDiagnostics.syncState ?? 'null'}
</Text>
Expand All @@ -434,9 +435,21 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
)}
</Box>
</Box>
<Box justifyContent="SpaceBetween">
<Text size="L400">Events</Text>
<Text size="L400">Total: {roomState.size}</Text>
<Box justifyContent="SpaceBetween" alignItems="Center">
<Text size="L400">Events (Total: {roomState.size})</Text>
<Button
onClick={handleFetchFullState}
disabled={fetchingApiState}
variant="Secondary"
fill="Soft"
size="300"
radii="300"
outlined
>
<Text size="B300">
{fetchingApiState ? 'Fetching...' : 'Fetch Full State'}
</Text>
</Button>
</Box>
<CutoutCard>
<MenuItem
Expand Down Expand Up @@ -513,6 +526,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
setOpenStateEvent({
type: eventType,
stateKey,
rawEvent: stateKeyToEvents.get(stateKey)?.event,
});
}}
key={stateKey}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,18 @@ function StateEventView({ content, eventJSONStr, onEditContent }: StateEventView
export type StateEventInfo = {
type: string;
stateKey: string;
rawEvent?: object;
};
export type StateEventEditorProps = StateEventInfo & {
requestClose: () => void;
};

export function StateEventEditor({ type, stateKey, requestClose }: StateEventEditorProps) {
export function StateEventEditor({
type,
stateKey,
rawEvent,
requestClose,
}: StateEventEditorProps) {
const mx = useMatrixClient();
const room = useRoom();
const stateEvent = useStateEvent(room, type as keyof StateEvents, stateKey);
Expand All @@ -252,9 +258,16 @@ export function StateEventEditor({ type, stateKey, requestClose }: StateEventEdi
const canEdit = permissions.stateEvent(type, mx.getSafeUserId());

const eventJSONStr = useMemo(() => {
if (!stateEvent) return '';
return JSON.stringify(stateEvent.event, null, EDITOR_INTENT_SPACE_COUNT);
}, [stateEvent]);
if (stateEvent) {
return JSON.stringify(stateEvent.event, null, EDITOR_INTENT_SPACE_COUNT);
}
if (rawEvent) {
return JSON.stringify(rawEvent, null, EDITOR_INTENT_SPACE_COUNT);
}
return '';
}, [stateEvent, rawEvent]);

const content = stateEvent?.getContent() ?? (rawEvent as { content?: object })?.content ?? {};

const handleCloseEdit = useCallback(() => {
setEditContent(undefined);
Expand Down Expand Up @@ -286,7 +299,7 @@ export function StateEventEditor({ type, stateKey, requestClose }: StateEventEdi
/>
) : (
<StateEventView
content={stateEvent?.getContent() ?? {}}
content={content}
onEditContent={canEdit ? setEditContent : undefined}
eventJSONStr={eventJSONStr}
/>
Expand Down
24 changes: 1 addition & 23 deletions src/app/features/settings/developer-tools/SyncDiagnostics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,6 @@ const formatListCoverage = (knownCount: number, rangeEnd: number): string => {
return `${loadedCount}/${knownCount}`;
};

const formatSyncReason = (reason: string): string => {
if (reason === 'sliding_active') return 'Sliding Sync active';
if (reason === 'sliding_disabled_server') return 'Server-side sliding sync disabled';
if (reason === 'session_opt_out') return 'Session opt-in is off';
if (reason === 'missing_proxy') return 'Sliding proxy URL missing';
if (reason === 'cold_cache_bootstrap') return 'Cold-cache bootstrap (classic for this run)';
if (reason === 'probe_failed_fallback') return 'Sliding probe failed, using fallback';
return reason;
};

export function SyncDiagnostics() {
const mx = useMatrixClient();
const [, setTick] = useState(0);
Expand All @@ -148,20 +138,8 @@ export function SyncDiagnostics() {
gap="100"
>
<Box direction="Column" gap="100" style={{ padding: '12px' }}>
<Text size="T300">
Transport: {diagnostics.transport}
{diagnostics.fallbackFromSliding ? ' (fallback)' : ''}
</Text>
<Text size="T300">Transport: {diagnostics.transport}</Text>
<Text size="T300">State: {diagnostics.syncState ?? 'null'}</Text>
<Text size="T300">
Sliding configured: {diagnostics.slidingConfigured ? 'yes' : 'no'}
</Text>
<Text size="T300">
Sliding server-enabled: {diagnostics.slidingEnabledOnServer ? 'yes' : 'no'}
</Text>
<Text size="T300">Sliding session opt-in: {diagnostics.sessionOptIn ? 'yes' : 'no'}</Text>
<Text size="T300">Sliding requested: {diagnostics.slidingRequested ? 'yes' : 'no'}</Text>
<Text size="T300">Sync reason: {formatSyncReason(diagnostics.reason)}</Text>
<Text size="T300">
Room counts: {roomDiagnostics.totalRooms} total, {roomDiagnostics.joinedRooms} joined,{' '}
{roomDiagnostics.inviteRooms} invites
Expand Down
16 changes: 3 additions & 13 deletions src/app/hooks/useSlidingSyncActiveRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,6 @@ import { useMatrixClient } from '$hooks/useMatrixClient';
import { getSlidingSyncManager } from '$client/initMatrix';
import { useSelectedRoom } from '$hooks/router/useSelectedRoom';

/**
* Subscribes the currently selected room to the sliding sync "active room"
* custom subscription (higher timeline limit) for the duration the room is open.
*
* Subscriptions are intentionally never removed on navigation — once a room
* has been opened it continues receiving background updates so that returning
* to it is instant. Explicit unsubscription (and timeline pruning) only happens
* when the user actually leaves the room via `unsubscribeFromRoom()`.
*
* Safe to call unconditionally — it is a no-op when classic sync is in use
* (i.e. when there is no SlidingSyncManager for the client).
*/
export const useSlidingSyncActiveRoom = (): void => {
const mx = useMatrixClient();
const roomId = useSelectedRoom();
Expand All @@ -25,6 +13,8 @@ export const useSlidingSyncActiveRoom = (): void => {
if (!manager) return undefined;

manager.subscribeToRoom(roomId);
return undefined;
return () => {
manager.unsubscribeFromRoom(roomId);
};
}, [mx, roomId]);
};
7 changes: 3 additions & 4 deletions src/app/pages/client/ClientNonUIFeatures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ReactNode } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import type { RoomEventHandlerMap } from '$types/matrix-sdk';
import { getPresenceSyncManager } from '$client/initMatrix';
import {
MatrixEvent,
MatrixEventEvent,
Expand Down Expand Up @@ -53,7 +54,6 @@ import {
import { mobileOrTablet } from '$utils/user-agent';
import { createDebugLogger } from '$utils/debugLogger';
import { useSlidingSyncActiveRoom } from '$hooks/useSlidingSyncActiveRoom';
import { getSlidingSyncManager } from '$client/initMatrix';
import { NotificationBanner } from '$components/notification-banner';
import { ThemeMigrationBanner } from '$components/theme/ThemeMigrationBanner';
import { TelemetryConsentBanner } from '$components/telemetry-consent';
Expand Down Expand Up @@ -851,11 +851,10 @@ function PresenceFeature() {
const [sendPresence] = useSetting(settingsAtom, 'sendPresence');

useEffect(() => {
// Classic sync: set_presence query param on every /sync poll.
// Classic sync / MSC4186 presence: set_presence query param on every /sync poll.
// Passing undefined restores the default (online); Offline suppresses broadcasting.
mx.setSyncPresence(sendPresence ? undefined : SetPresence.Offline);
// Sliding sync: enable/disable the presence extension on the next poll.
getSlidingSyncManager(mx)?.setPresenceEnabled(sendPresence);
getPresenceSyncManager(mx)?.setPresenceEnabled(sendPresence);
}, [mx, sendPresence]);

return null;
Expand Down
3 changes: 2 additions & 1 deletion src/app/pages/client/SidebarNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,9 @@ export function SidebarNav() {
}
sticky={
<SidebarStack>
<UnverifiedTab />
{oldSidebar ? (
<>
<UnverifiedTab />
<SearchTab />
<InboxTab />
<div style={{ paddingBottom: config.space.S100 }}>
Expand All @@ -159,6 +159,7 @@ export function SidebarNav() {
<>
{isCollapsed && (
<>
<UnverifiedTab />
<SearchTab />
<InboxTab />
<SettingsTab />
Expand Down
2 changes: 2 additions & 0 deletions src/app/pages/client/sidebar/UserQuickTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AccountSwitcherTab } from './AccountSwitcherTab';
import { InboxTab } from './InboxTab';
import { SearchTab } from './SearchTab';
import { SettingsTab } from './SettingsTab';
import { UnverifiedTab } from './UnverifiedTab';
import { useAtom } from 'jotai';
import { isResizingSidebarAtom } from '$state/isResizingSidebar';
import * as css from './UserQuickTools.css';
Expand Down Expand Up @@ -46,6 +47,7 @@ export function UserQuickTools({
>
{!isCollapsed && (
<>
<UnverifiedTab isBottom />
<InboxTab isBottom />
<SearchTab isBottom />
<SettingsTab isBottom />
Expand Down
Loading
Loading