diff --git a/src/app/components/AuthSkeleton.tsx b/src/app/components/AuthSkeleton.tsx
new file mode 100644
index 000000000..1ba659ccc
--- /dev/null
+++ b/src/app/components/AuthSkeleton.tsx
@@ -0,0 +1,69 @@
+import React, { useId } from 'react';
+
+export function AuthSkeleton() {
+ const id = useId().replace(/:/g, '');
+ const shimmerKeyframes = `
+ @keyframes shimmer-${id} {
+ 0% { background-position: -400px 0; }
+ 100% { background-position: 400px 0; }
+ }
+ `;
+
+ const shimmer = {
+ background:
+ 'linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%)',
+ backgroundSize: '800px 100%',
+ animation: `shimmer-${id} 1.6s ease-in-out infinite`,
+ borderRadius: '4px',
+ } as React.CSSProperties;
+
+ return (
+ <>
+
+
+ {/* Card */}
+
+ {/* Logo + app name */}
+
+
+ {/* Server picker */}
+
+
+ {/* Form fields */}
+
+
+
+ >
+ );
+}
diff --git a/src/app/components/LobbySkeleton.tsx b/src/app/components/LobbySkeleton.tsx
new file mode 100644
index 000000000..85eeac014
--- /dev/null
+++ b/src/app/components/LobbySkeleton.tsx
@@ -0,0 +1,111 @@
+import React, { useId } from 'react';
+
+const ROOM_ROWS = [
+ { w: '160px', indent: false },
+ { w: '120px', indent: true },
+ { w: '140px', indent: true },
+ { w: '130px', indent: true },
+ { w: '150px', indent: false },
+ { w: '110px', indent: true },
+];
+
+export function LobbySkeleton() {
+ const id = useId().replace(/:/g, '');
+ const shimmerKeyframes = `
+ @keyframes shimmer-${id} {
+ 0% { background-position: -400px 0; }
+ 100% { background-position: 400px 0; }
+ }
+ `;
+
+ const shimmer = {
+ background:
+ 'linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%)',
+ backgroundSize: '800px 100%',
+ animation: `shimmer-${id} 1.6s ease-in-out infinite`,
+ borderRadius: '4px',
+ } as React.CSSProperties;
+
+ return (
+ <>
+
+
+ {/* Header — matches LobbyHeader (56px) */}
+
+
+
+ {/* Hero — matches PageHero with large avatar + title + subtitle */}
+
+
+ {/* Room list rows */}
+
+ {ROOM_ROWS.map((row, i) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+ ))}
+
+
+
+ >
+ );
+}
diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx
index fd3aa826d..89f1e73f3 100644
--- a/src/app/features/room/RoomTimeline.tsx
+++ b/src/app/features/room/RoomTimeline.tsx
@@ -557,7 +557,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
base += len;
return seg;
});
- }, [timeline.linkedTimelines]);
+ }, [timeline.linkedTimelines, eventsLength]);
const liveTimelineLinked =
timeline.linkedTimelines[timeline.linkedTimelines.length - 1] === getLiveTimeline(room);
const canPaginateBack =
diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx
index a70faa521..66b7340fb 100644
--- a/src/app/pages/Router.tsx
+++ b/src/app/pages/Router.tsx
@@ -8,6 +8,8 @@ import {
redirect,
} from 'react-router-dom';
import { RoomSkeleton } from '../components/RoomSkeleton';
+import { LobbySkeleton } from '../components/LobbySkeleton';
+import { AuthSkeleton } from '../components/AuthSkeleton';
import { ClientConfig } from '../hooks/useClientConfig';
import {
@@ -124,7 +126,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
return null;
}}
element={
-
+ }>
@@ -309,7 +311,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
+ }>
}