Skip to content

Commit 9de2775

Browse files
author
Aleksey
committed
fix: gate room rendering on timeline readiness (Continuwuity #779)
1 parent 6a05ff5 commit 9de2775

5 files changed

Lines changed: 98 additions & 4 deletions

File tree

src/app/components/RoomLoading.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react';
2+
import { Box, Spinner, Text } from 'folds';
3+
4+
export function RoomLoading() {
5+
return (
6+
<Box
7+
grow="Yes"
8+
alignItems="Center"
9+
justifyContent="Center"
10+
gap="300"
11+
direction="Column"
12+
>
13+
<Spinner variant="Secondary" size="600" />
14+
<Text size="T300">Loading room…</Text>
15+
</Box>
16+
);
17+
}

src/app/hooks/useRoomReady.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useEffect, useState } from 'react';
2+
import { Room, RoomEvent } from 'matrix-js-sdk';
3+
4+
/**
5+
* Returns true once a room has at least one event in its live timeline.
6+
*
7+
* After joining a room the first /sync response may arrive before the
8+
* homeserver has finished writing all state, so the Room object can exist
9+
* in the SDK store with an empty timeline. Rendering that room immediately
10+
* leads to an infinite spinner (Continuwuity #779).
11+
*
12+
* This hook watches for incoming timeline events and flips to `true` as
13+
* soon as the room has data, so components can gate rendering on it.
14+
*/
15+
export function useRoomReady(room: Room): boolean {
16+
const [ready, setReady] = useState(
17+
() => room.getLiveTimeline().getEvents().length > 0
18+
);
19+
20+
useEffect(() => {
21+
if (ready) return undefined;
22+
23+
// Check again — events may have arrived between render and effect.
24+
if (room.getLiveTimeline().getEvents().length > 0) {
25+
setReady(true);
26+
return undefined;
27+
}
28+
29+
const onTimeline = () => {
30+
if (room.getLiveTimeline().getEvents().length > 0) {
31+
setReady(true);
32+
}
33+
};
34+
35+
room.on(RoomEvent.Timeline, onTimeline);
36+
return () => {
37+
room.removeListener(RoomEvent.Timeline, onTimeline);
38+
};
39+
}, [room, ready]);
40+
41+
return ready;
42+
}

src/app/pages/client/direct/RoomProvider.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import React, { ReactNode } from 'react';
22
import { useParams } from 'react-router-dom';
3+
import { Room } from 'matrix-js-sdk';
34
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
45
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
56
import { useMatrixClient } from '../../../hooks/useMatrixClient';
67
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
78
import { useDirectRooms } from './useDirectRooms';
9+
import { useRoomReady } from '../../../hooks/useRoomReady';
10+
import { RoomLoading } from '../../../components/RoomLoading';
811

912
export function DirectRouteRoomProvider({ children }: { children: ReactNode }) {
1013
const mx = useMatrixClient();
@@ -20,7 +23,15 @@ export function DirectRouteRoomProvider({ children }: { children: ReactNode }) {
2023

2124
return (
2225
<RoomProvider key={room.roomId} value={room}>
23-
<IsDirectRoomProvider value>{children}</IsDirectRoomProvider>
26+
<IsDirectRoomProvider value>
27+
<RoomReadyGate room={room}>{children}</RoomReadyGate>
28+
</IsDirectRoomProvider>
2429
</RoomProvider>
2530
);
2631
}
32+
33+
function RoomReadyGate({ room, children }: { room: Room; children: ReactNode }) {
34+
const ready = useRoomReady(room);
35+
if (!ready) return <RoomLoading />;
36+
return <>{children}</>;
37+
}

src/app/pages/client/home/RoomProvider.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import React, { ReactNode } from 'react';
22
import { useParams } from 'react-router-dom';
3+
import { Room } from 'matrix-js-sdk';
34
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
45
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
56
import { useMatrixClient } from '../../../hooks/useMatrixClient';
67
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
78
import { useHomeRooms } from './useHomeRooms';
89
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
10+
import { useRoomReady } from '../../../hooks/useRoomReady';
11+
import { RoomLoading } from '../../../components/RoomLoading';
912

1013
export function HomeRouteRoomProvider({ children }: { children: ReactNode }) {
1114
const mx = useMatrixClient();
@@ -28,7 +31,15 @@ export function HomeRouteRoomProvider({ children }: { children: ReactNode }) {
2831

2932
return (
3033
<RoomProvider key={room.roomId} value={room}>
31-
<IsDirectRoomProvider value={false}>{children}</IsDirectRoomProvider>
34+
<IsDirectRoomProvider value={false}>
35+
<RoomReadyGate room={room}>{children}</RoomReadyGate>
36+
</IsDirectRoomProvider>
3237
</RoomProvider>
3338
);
3439
}
40+
41+
function RoomReadyGate({ room, children }: { room: Room; children: ReactNode }) {
42+
const ready = useRoomReady(room);
43+
if (!ready) return <RoomLoading />;
44+
return <>{children}</>;
45+
}

src/app/pages/client/space/RoomProvider.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { ReactNode } from 'react';
22
import { useParams } from 'react-router-dom';
33
import { useAtom, useAtomValue } from 'jotai';
4+
import { Room } from 'matrix-js-sdk';
45
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
56
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
67
import { useMatrixClient } from '../../../hooks/useMatrixClient';
@@ -13,6 +14,8 @@ import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParams
1314
import { mDirectAtom } from '../../../state/mDirectList';
1415
import { settingsAtom } from '../../../state/settings';
1516
import { useSetting } from '../../../state/hooks/settings';
17+
import { useRoomReady } from '../../../hooks/useRoomReady';
18+
import { RoomLoading } from '../../../components/RoomLoading';
1619

1720
export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
1821
const mx = useMatrixClient();
@@ -42,7 +45,9 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
4245
// allow to view space timeline
4346
return (
4447
<RoomProvider key={room.roomId} value={room}>
45-
<IsDirectRoomProvider value={mDirects.has(room.roomId)}>{children}</IsDirectRoomProvider>
48+
<IsDirectRoomProvider value={mDirects.has(room.roomId)}>
49+
<RoomReadyGate room={room}>{children}</RoomReadyGate>
50+
</IsDirectRoomProvider>
4651
</RoomProvider>
4752
);
4853
}
@@ -68,7 +73,15 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
6873

6974
return (
7075
<RoomProvider key={room.roomId} value={room}>
71-
<IsDirectRoomProvider value={mDirects.has(room.roomId)}>{children}</IsDirectRoomProvider>
76+
<IsDirectRoomProvider value={mDirects.has(room.roomId)}>
77+
<RoomReadyGate room={room}>{children}</RoomReadyGate>
78+
</IsDirectRoomProvider>
7279
</RoomProvider>
7380
);
7481
}
82+
83+
function RoomReadyGate({ room, children }: { room: Room; children: ReactNode }) {
84+
const ready = useRoomReady(room);
85+
if (!ready) return <RoomLoading />;
86+
return <>{children}</>;
87+
}

0 commit comments

Comments
 (0)