fix: sent messages not appearing + add Lobby/Auth skeleton loaders
CI / Build & Quality Checks (push) Failing after 5m45s
CI / Build & Quality Checks (push) Failing after 5m45s
- Fix timelineSegments useMemo stale cache: the Perf-5 optimization used timeline.linkedTimelines as its only dep, but that reference never changes when events are added in-place; adding eventsLength as a dep makes it recompute on every new live event so the binary search always finds the new item - Add LobbySkeleton: shimmer placeholder for space lobby (header + hero + room list rows) shown while the Lobby chunk lazy-loads - Add AuthSkeleton: shimmer placeholder for auth pages (logo + server picker + form fields) shown while AuthLayout chunk lazy-loads - Wire both into Router.tsx fallback props Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||||
|
<>
|
||||||
|
<style>{shimmerKeyframes}</style>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minHeight: '100dvh',
|
||||||
|
padding: '16px',
|
||||||
|
'--skeleton-base': 'color-mix(in srgb, currentColor 8%, transparent)',
|
||||||
|
'--skeleton-shine': 'color-mix(in srgb, currentColor 15%, transparent)',
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Card */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '360px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Logo + app name */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||||
|
<div style={{ ...shimmer, width: '64px', height: '64px', borderRadius: '50%' }} />
|
||||||
|
<div style={{ ...shimmer, width: '100px', height: '20px' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Server picker */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
|
<div style={{ ...shimmer, width: '80px', height: '12px' }} />
|
||||||
|
<div style={{ ...shimmer, width: '100%', height: '40px', borderRadius: '8px' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form fields */}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
|
<div style={{ ...shimmer, width: '100%', height: '40px', borderRadius: '8px' }} />
|
||||||
|
<div style={{ ...shimmer, width: '100%', height: '40px', borderRadius: '8px' }} />
|
||||||
|
<div style={{ ...shimmer, width: '100%', height: '40px', borderRadius: '8px' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<style>{shimmerKeyframes}</style>
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flexGrow: 1,
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'--skeleton-base': 'color-mix(in srgb, currentColor 8%, transparent)',
|
||||||
|
'--skeleton-shine': 'color-mix(in srgb, currentColor 15%, transparent)',
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Header — matches LobbyHeader (56px) */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '56px',
|
||||||
|
borderBottom: '1px solid color-mix(in srgb, currentColor 10%, transparent)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
padding: '0 16px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ ...shimmer, width: '32px', height: '32px', borderRadius: '50%', flexShrink: 0 }} />
|
||||||
|
<div style={{ ...shimmer, width: '130px', height: '16px' }} />
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
<div style={{ ...shimmer, width: '24px', height: '24px', borderRadius: '4px' }} />
|
||||||
|
<div style={{ ...shimmer, width: '24px', height: '24px', borderRadius: '4px' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, overflowY: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{/* Hero — matches PageHero with large avatar + title + subtitle */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '32px 16px 24px',
|
||||||
|
gap: '12px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ ...shimmer, width: '72px', height: '72px', borderRadius: '50%' }} />
|
||||||
|
<div style={{ ...shimmer, width: '180px', height: '20px' }} />
|
||||||
|
<div style={{ ...shimmer, width: '240px', height: '13px' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Room list rows */}
|
||||||
|
<div style={{ flex: 1, padding: '8px 0' }}>
|
||||||
|
{ROOM_ROWS.map((row, i) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
padding: `6px 16px 6px ${row.indent ? '36px' : '16px'}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...shimmer,
|
||||||
|
width: '18px',
|
||||||
|
height: '18px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ ...shimmer, width: row.w, height: '14px' }} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -557,7 +557,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||||||
base += len;
|
base += len;
|
||||||
return seg;
|
return seg;
|
||||||
});
|
});
|
||||||
}, [timeline.linkedTimelines]);
|
}, [timeline.linkedTimelines, eventsLength]);
|
||||||
const liveTimelineLinked =
|
const liveTimelineLinked =
|
||||||
timeline.linkedTimelines[timeline.linkedTimelines.length - 1] === getLiveTimeline(room);
|
timeline.linkedTimelines[timeline.linkedTimelines.length - 1] === getLiveTimeline(room);
|
||||||
const canPaginateBack =
|
const canPaginateBack =
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
redirect,
|
redirect,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { RoomSkeleton } from '../components/RoomSkeleton';
|
import { RoomSkeleton } from '../components/RoomSkeleton';
|
||||||
|
import { LobbySkeleton } from '../components/LobbySkeleton';
|
||||||
|
import { AuthSkeleton } from '../components/AuthSkeleton';
|
||||||
|
|
||||||
import { ClientConfig } from '../hooks/useClientConfig';
|
import { ClientConfig } from '../hooks/useClientConfig';
|
||||||
import {
|
import {
|
||||||
@@ -124,7 +126,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||||||
return null;
|
return null;
|
||||||
}}
|
}}
|
||||||
element={
|
element={
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={<AuthSkeleton />}>
|
||||||
<AuthLayout />
|
<AuthLayout />
|
||||||
<UnAuthRouteThemeManager />
|
<UnAuthRouteThemeManager />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
@@ -309,7 +311,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||||||
<Route
|
<Route
|
||||||
path={_LOBBY_PATH}
|
path={_LOBBY_PATH}
|
||||||
element={
|
element={
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={<LobbySkeleton />}>
|
||||||
<Lobby />
|
<Lobby />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user