Skip to content

Commit 855cfe5

Browse files
authored
Merge pull request #98 from rockfactory/fix/realtime-ui-and-offline
fix: realtime UI and offline handling
2 parents 3947915 + edcf150 commit 855cfe5

8 files changed

Lines changed: 313 additions & 209 deletions

src/games/sync/ui/OnlinePeers.tsx

Lines changed: 0 additions & 142 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.identiconBadge {
2+
width: 18px;
3+
height: 18px;
4+
border-radius: 50%;
5+
background: var(--mantine-color-dark-5);
6+
border: 2px solid var(--mantine-color-dark-7);
7+
display: inline-flex;
8+
align-items: center;
9+
justify-content: center;
10+
overflow: hidden;
11+
flex-shrink: 0;
12+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { Avatar, Box, Divider, Group, Stack, Text } from '@mantine/core';
2+
import { IconCircleFilled } from '@tabler/icons-react';
3+
import { useMemo } from 'react';
4+
import { useAllPeers } from '../peersSlice';
5+
import { SENDER_ID } from '../realtimeSyncTypes';
6+
import { DeviceIdenticon } from './DeviceIdenticon';
7+
import classes from './OnlinePeersList.module.css';
8+
9+
const IDENTICON_SIZE = 14;
10+
11+
export function OnlinePeersList() {
12+
const peers = useAllPeers();
13+
14+
const leaderSenderId = useMemo(() => {
15+
if (peers.length === 0) return null;
16+
return [...peers.map(p => p.senderId)].sort()[0];
17+
}, [peers]);
18+
19+
const groupedByUser = useMemo(() => {
20+
const groups = new Map<
21+
string,
22+
{
23+
userId: string;
24+
displayName: string;
25+
avatarUrl: string | null;
26+
devices: typeof peers;
27+
}
28+
>();
29+
for (const p of peers) {
30+
const existing = groups.get(p.userId);
31+
if (existing) {
32+
existing.devices.push(p);
33+
} else {
34+
groups.set(p.userId, {
35+
userId: p.userId,
36+
displayName: p.displayName,
37+
avatarUrl: p.avatarUrl,
38+
devices: [p],
39+
});
40+
}
41+
}
42+
return Array.from(groups.values());
43+
}, [peers]);
44+
45+
return (
46+
<Stack gap="xs" miw={220}>
47+
{groupedByUser.map((group, idx) => (
48+
<Box key={group.userId || group.displayName}>
49+
{idx > 0 && <Divider mb="xs" />}
50+
<Group gap="xs" wrap="nowrap" mb={4}>
51+
<Avatar src={group.avatarUrl} size={22} radius="xl">
52+
{group.displayName.charAt(0).toUpperCase()}
53+
</Avatar>
54+
<Text size="sm" fw={500}>
55+
{group.displayName}
56+
{group.devices.some(d => d.senderId === SENDER_ID) && (
57+
<Text span size="xs" c="dimmed">
58+
{' '}
59+
(you)
60+
</Text>
61+
)}
62+
</Text>
63+
</Group>
64+
<Stack gap={4} pl={30}>
65+
{group.devices.map(device => (
66+
<Group key={device.senderId} gap={6} wrap="nowrap">
67+
<Box className={classes.identiconBadge}>
68+
<DeviceIdenticon
69+
seed={device.deviceName}
70+
size={IDENTICON_SIZE}
71+
/>
72+
</Box>
73+
<Text size="xs" c="dimmed">
74+
{device.deviceName || 'Unknown device'}
75+
</Text>
76+
{device.senderId === leaderSenderId && (
77+
<IconCircleFilled
78+
size={8}
79+
color="var(--mantine-color-green-5)"
80+
title="Leader"
81+
/>
82+
)}
83+
</Group>
84+
))}
85+
</Stack>
86+
</Box>
87+
))}
88+
</Stack>
89+
);
90+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.wrap {
2+
position: relative;
3+
display: inline-flex;
4+
width: var(--peer-avatar-size);
5+
height: var(--peer-avatar-size);
6+
vertical-align: middle;
7+
}
8+
9+
.badge {
10+
position: absolute;
11+
bottom: -2px;
12+
right: -2px;
13+
width: var(--peer-avatar-badge-size);
14+
height: var(--peer-avatar-badge-size);
15+
border-radius: 50%;
16+
border: 2px solid var(--mantine-color-dark-7);
17+
background: var(--mantine-color-dark-5);
18+
overflow: hidden;
19+
display: flex;
20+
align-items: center;
21+
justify-content: center;
22+
}

src/games/sync/ui/PeerAvatar.tsx

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Avatar, Box, Tooltip } from '@mantine/core';
2+
import type { CSSProperties } from 'react';
23
import type { PeerInfo } from '../peersSlice';
34
import { SENDER_ID } from '../realtimeSyncTypes';
45
import { DeviceIdenticon } from './DeviceIdenticon';
6+
import classes from './PeerAvatar.module.css';
57

68
export interface PeerAvatarProps {
79
peer: PeerInfo;
@@ -35,31 +37,16 @@ export function PeerAvatar({
3537
const content =
3638
showDeviceBadge && peer.deviceName ? (
3739
<Box
38-
pos="relative"
39-
style={{
40-
display: 'inline-flex',
41-
width: size,
42-
height: size,
43-
verticalAlign: 'middle',
44-
}}
40+
className={classes.wrap}
41+
style={
42+
{
43+
'--peer-avatar-size': `${size}px`,
44+
'--peer-avatar-badge-size': `${badgeSize}px`,
45+
} as CSSProperties
46+
}
4547
>
4648
{avatar}
47-
<Box
48-
pos="absolute"
49-
style={{
50-
bottom: -2,
51-
right: -2,
52-
width: badgeSize,
53-
height: badgeSize,
54-
borderRadius: '50%',
55-
border: '2px solid var(--mantine-color-dark-7)',
56-
background: 'var(--mantine-color-dark-5)',
57-
overflow: 'hidden',
58-
display: 'flex',
59-
alignItems: 'center',
60-
justifyContent: 'center',
61-
}}
62-
>
49+
<Box className={classes.badge}>
6350
<DeviceIdenticon
6451
seed={peer.deviceName}
6552
size={badgeSize - 4}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
.pill {
2+
display: inline-flex;
3+
align-items: center;
4+
gap: 10px;
5+
padding: 4px 6px 4px 10px;
6+
border-radius: 999px;
7+
background: var(--mantine-color-dark-6);
8+
border: 1px solid var(--mantine-color-dark-4);
9+
transition:
10+
background 120ms ease,
11+
border-color 120ms ease;
12+
}
13+
14+
.pill:not(:disabled):hover {
15+
background: var(--mantine-color-dark-5);
16+
border-color: var(--mantine-color-dark-3);
17+
}
18+
19+
.pill:disabled {
20+
cursor: default;
21+
}
22+
23+
.statusIconWrap {
24+
position: relative;
25+
display: inline-flex;
26+
}
27+
28+
.statusDot {
29+
position: absolute;
30+
bottom: -1px;
31+
right: -1px;
32+
width: 7px;
33+
height: 7px;
34+
border-radius: 50%;
35+
background: var(--mantine-color-green-5);
36+
border: 1.5px solid var(--mantine-color-dark-6);
37+
box-shadow: 0 0 6px var(--mantine-color-green-5);
38+
}
39+
40+
.avatarStack {
41+
display: inline-flex;
42+
align-items: center;
43+
}
44+
45+
.avatarStackItem {
46+
position: relative;
47+
z-index: var(--peer-z, 0);
48+
}
49+
50+
.avatarStackItem:not(:first-child) {
51+
margin-left: -10px;
52+
}

0 commit comments

Comments
 (0)