Skip to content

Commit e2205ce

Browse files
committed
add connection indicator
1 parent fc99a1c commit e2205ce

File tree

7 files changed

+144
-8
lines changed

7 files changed

+144
-8
lines changed

src/components/eventStream/EventStream.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import { useSnackbar } from "notistack";
1313

1414
import { useASAuthorizationStatus } from "../../hooks/useIsAuthorized";
1515
import { getMessageFromEvent } from "../../protobuf/protobuf";
16-
import { eventStreamEnabledAtom, useEventStream } from "../../state/eventStream";
16+
import {
17+
eventStreamEnabledAtom,
18+
useEventStream,
19+
webSocketStatusAtom,
20+
} from "../../state/eventStream";
1721
import { useUnreadEventCount } from "../../state/notifications";
1822
import { EventMessage } from "../eventMessages/EventMessage";
1923
import { useIsEventStreamInstalled } from "./useIsEventStreamInstalled";
@@ -26,6 +30,10 @@ dayjs.extend(utc);
2630
const buildWebSocketUrl = (location: string): string => {
2731
const url = new URL(location);
2832
url.protocol = "wss:";
33+
34+
// Add ordinal parameter to get all historical messages
35+
url.searchParams.set("stream_from_ordinal", "0");
36+
2937
return url.toString();
3038
};
3139

@@ -49,6 +57,7 @@ export const EventStream = () => {
4957
});
5058

5159
const [eventStreamEnabled] = useAtom(eventStreamEnabledAtom);
60+
const [, setWebSocketStatus] = useAtom(webSocketStatusAtom);
5261

5362
const handleWebSocketMessage = useCallback(
5463
(event: MessageEvent) => {
@@ -101,7 +110,7 @@ export const EventStream = () => {
101110
// Build WebSocket URL
102111
const wsUrl = eventStreamEnabled && asRole && location ? buildWebSocketUrl(location) : null;
103112

104-
useWebSocket(wsUrl, {
113+
const { readyState } = useWebSocket(wsUrl, {
105114
onOpen: handleWebSocketOpen,
106115
onClose: handleWebSocketClose,
107116
onError: handleWebSocketError,
@@ -112,6 +121,11 @@ export const EventStream = () => {
112121
reconnectInterval: 3000,
113122
});
114123

124+
// Expose connection status for status indicator
125+
useEffect(() => {
126+
setWebSocketStatus(readyState);
127+
}, [readyState, setWebSocketStatus]);
128+
115129
useEffect(() => {
116130
if (asRole && data) {
117131
setLocation(data);

src/components/eventStream/EventStreamMessages.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@ import { Box, Divider, Typography } from "@mui/material";
22

33
import { EventList } from "./EventList";
44
import { EventStreamToggle } from "./EventStreamToggle";
5+
import { WebSocketStatusIndicator } from "./WebSocketStatusIndicator";
56

67
/**
78
* Main event stream interface in the user menu popover
89
*/
910
export const EventStreamMessages = () => (
1011
<Box sx={{ minWidth: 300 }}>
11-
<Typography sx={{ mb: 1 }} variant="h6">
12+
<Typography sx={{ mb: 2 }} variant="h6">
1213
Event Stream
1314
</Typography>
1415

15-
<EventStreamToggle />
16+
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", mb: 1 }}>
17+
<EventStreamToggle />
18+
<WebSocketStatusIndicator />
19+
</Box>
1620

1721
<Divider sx={{ my: 2 }} />
1822

src/components/eventStream/EventStreamToggle.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ export const EventStreamToggle = () => {
2323
/>
2424
}
2525
label={`Event stream ${isEventStreamInstalled ? "(alpha)" : "(not available)"}`}
26-
sx={{ mb: 2 }}
26+
sx={{
27+
margin: 0,
28+
alignItems: "center",
29+
"& .MuiFormControlLabel-label": { fontSize: "0.875rem", lineHeight: 1.2 },
30+
}}
2731
/>
2832
);
2933
};
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { FiberManualRecord } from "@mui/icons-material";
2+
import { Box, Tooltip, Typography } from "@mui/material";
3+
import { useAtom } from "jotai";
4+
5+
import { getWebSocketStatusFlags, webSocketStatusAtom } from "../../state/eventStream";
6+
7+
const STATUS_CONFIG = {
8+
connected: {
9+
color: "success.main",
10+
text: "Connected",
11+
tooltip: "Event stream is connected and receiving messages",
12+
},
13+
connecting: {
14+
color: "warning.main",
15+
text: "Connecting...",
16+
tooltip: "Connecting to event stream...",
17+
},
18+
reconnecting: {
19+
color: "warning.main",
20+
text: "Reconnecting...",
21+
tooltip: "Reconnecting to event stream...",
22+
},
23+
disconnected: {
24+
color: "error.main",
25+
text: "Disconnected",
26+
tooltip: "Event stream is disconnected",
27+
},
28+
} as const;
29+
30+
/**
31+
* WebSocket connection status indicator
32+
*/
33+
export const WebSocketStatusIndicator = () => {
34+
const [readyState] = useAtom(webSocketStatusAtom);
35+
const status = getWebSocketStatusFlags(readyState);
36+
37+
const getStatusKey = () => {
38+
if (status.isConnected) {
39+
return "connected";
40+
}
41+
if (status.isConnecting) {
42+
return "connecting";
43+
}
44+
if (status.isReconnecting) {
45+
return "reconnecting";
46+
}
47+
return "disconnected";
48+
};
49+
50+
const statusKey = getStatusKey();
51+
const config = STATUS_CONFIG[statusKey];
52+
53+
return (
54+
<Tooltip arrow title={config.tooltip}>
55+
<Box
56+
sx={{
57+
display: "flex",
58+
alignItems: "center",
59+
gap: 0.5,
60+
height: "fit-content",
61+
minHeight: "40px", // Match switch height
62+
justifyContent: "center",
63+
}}
64+
>
65+
<FiberManualRecord
66+
sx={{
67+
fontSize: 10,
68+
color: config.color,
69+
animation:
70+
status.isConnecting || status.isReconnecting
71+
? "pulse 1.5s ease-in-out infinite"
72+
: "none",
73+
"@keyframes pulse": {
74+
"0%": { opacity: 1 },
75+
"50%": { opacity: 0.5 },
76+
"100%": { opacity: 1 },
77+
},
78+
}}
79+
/>
80+
<Typography
81+
color="text.secondary"
82+
sx={{ fontSize: "0.75rem", lineHeight: 1.2, whiteSpace: "nowrap" }}
83+
variant="caption"
84+
>
85+
{config.text}
86+
</Typography>
87+
</Box>
88+
</Tooltip>
89+
);
90+
};

src/layouts/navigation/UserMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const UserMenu = () => {
4444
<Popper
4545
transition
4646
placement="bottom-start"
47-
sx={{ "& .MuiPopover-paper": { p: 1 } }}
47+
sx={{ "& .MuiPopover-paper": { p: 1 }, zIndex: (theme) => theme.zIndex.appBar + 1 }}
4848
{...bindPopper(popupState)}
4949
// anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
5050
// transformOrigin={{ vertical: "top", horizontal: "left" }}

src/layouts/navigation/UserMenuContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const UserMenuContentInner = () => {
4242
<Typography sx={{ fontWeight: "bold" }}>{user.username}</Typography>
4343
<Box>
4444
Roles:
45-
<Chips>
45+
<Chips sx={{ justifyContent: "center" }}>
4646
<Chip label={dmRole ?? "No DM Role"} size="small" />
4747
<Chip label={asRole ?? "No AS Role"} size="small" />
4848
</Chips>

src/state/eventStream.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ReadyState } from "react-use-websocket";
2+
13
import dayjs from "dayjs";
24
import utc from "dayjs/plugin/utc";
35
import { atom, useAtom } from "jotai";
@@ -16,6 +18,21 @@ export const eventsAtom = atom<ChargeMessage[]>([]);
1618
*/
1719
export const sessionStartTimeAtom = atom<dayjs.Dayjs | null>(null);
1820

21+
/**
22+
* Atom to track WebSocket connection status
23+
*/
24+
export const webSocketStatusAtom = atom<ReadyState>(ReadyState.CLOSED);
25+
26+
/**
27+
* Utility function to derive boolean status flags from ReadyState
28+
*/
29+
export const getWebSocketStatusFlags = (readyState: ReadyState) => ({
30+
isConnected: readyState === ReadyState.OPEN,
31+
isConnecting: readyState === ReadyState.CONNECTING,
32+
isDisconnected: readyState === ReadyState.CLOSED,
33+
isReconnecting: readyState === ReadyState.CLOSING,
34+
});
35+
1936
/**
2037
* Hook to manage events with deduplication and sorting
2138
*/
@@ -56,7 +73,14 @@ export const useEventStream = () => {
5673
setEvents([]);
5774
};
5875

59-
return { events, addEvent, isEventNewerThanSession, initializeSession, clearEvents };
76+
return {
77+
events,
78+
addEvent,
79+
isEventNewerThanSession,
80+
initializeSession,
81+
clearEvents,
82+
sessionStartTime,
83+
};
6084
};
6185

6286
/**

0 commit comments

Comments
 (0)