@tribe-nest/forge 3.42.1 → 3.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +2 -2
  2. package/src/data/queries/useCallChat.ts +162 -0
  3. package/src/data/queries/useCallRecording.ts +80 -0
  4. package/src/data/queries/useMembershipCalls.ts +162 -0
  5. package/src/i18n/de.json +73 -1
  6. package/src/i18n/en.json +73 -1
  7. package/src/index.ts +7 -0
  8. package/src/ui/headless/memberHome/index.ts +4 -0
  9. package/src/ui/headless/memberHome/useMemberCalls.ts +137 -0
  10. package/src/ui/headless/memberHome/useMemberHomeNav.ts +29 -5
  11. package/src/ui/media/CallStage.tsx +691 -553
  12. package/src/ui/media/PreJoin.tsx +269 -0
  13. package/src/ui/media/_tests/CallStage.spec.tsx +338 -5
  14. package/src/ui/media/_tests/callLayout.spec.ts +182 -0
  15. package/src/ui/media/_tests/callSignals.spec.ts +87 -0
  16. package/src/ui/media/_tests/callState.spec.ts +93 -1
  17. package/src/ui/media/_tests/fakeNode.ts +4 -1
  18. package/src/ui/media/callChat.ts +33 -0
  19. package/src/ui/media/callState.ts +125 -9
  20. package/src/ui/media/hooks/useCallSignals.ts +135 -0
  21. package/src/ui/media/hooks/useMeasuredWidth.ts +39 -0
  22. package/src/ui/media/index.ts +41 -1
  23. package/src/ui/media/panels/CallChatPanel.tsx +200 -0
  24. package/src/ui/media/panels/CallParticipantsPanel.tsx +125 -0
  25. package/src/ui/media/panels/CallSidePanel.tsx +152 -0
  26. package/src/ui/media/stage/CallCenterNotice.tsx +89 -0
  27. package/src/ui/media/stage/CallControlsBar.tsx +547 -0
  28. package/src/ui/media/stage/CallHealthPanel.tsx +132 -0
  29. package/src/ui/media/stage/CallToasts.tsx +49 -0
  30. package/src/ui/media/stage/CallTopBar.tsx +257 -0
  31. package/src/ui/media/stage/DeviceMenu.tsx +142 -0
  32. package/src/ui/media/stage/ReactionOverlay.tsx +38 -0
  33. package/src/ui/media/stage/SelfView.tsx +146 -0
  34. package/src/ui/media/stage/VideoTile.tsx +232 -0
  35. package/src/ui/media/stage/callLayout.ts +253 -0
  36. package/src/ui/media/stage/callSignals.ts +91 -0
  37. package/src/ui/media/stage/callSounds.ts +78 -0
  38. package/src/ui/media/stage/stageTheme.ts +50 -0
  39. package/src/ui/styled/forge-utilities.css +13 -0
  40. package/src/ui/styled/members/MemberCallsCalendar.tsx +571 -0
  41. package/src/ui/styled/members/_tests/MemberCallsCalendar.spec.tsx +135 -0
  42. package/src/ui/styled/members/index.ts +1 -0
@@ -0,0 +1,152 @@
1
+ import { X } from "lucide-react";
2
+ import type { RaisedHand } from "@tribe-nest/media-client";
3
+
4
+ import type { ForgeT } from "../../../i18n";
5
+ import type { CallStageChat } from "../callChat";
6
+ import type { CallTile } from "../callState";
7
+ import type { StageTokens } from "../stage/stageTheme";
8
+ import { CallChatPanel } from "./CallChatPanel";
9
+ import { CallParticipantsPanel } from "./CallParticipantsPanel";
10
+
11
+ export type SidePanelTab = "chat" | "people";
12
+
13
+ /**
14
+ * The right rail: one width, two tabs, the Zoom-familiar shape. Tabs rather
15
+ * than stacked panels because the rail's width is the stage's loss, and two
16
+ * rails would take a third of a laptop screen.
17
+ */
18
+ export function CallSidePanel({
19
+ tab,
20
+ onTab,
21
+ onClose,
22
+ tokens,
23
+ t,
24
+ chat,
25
+ tiles,
26
+ hands,
27
+ isHost,
28
+ myIdentity,
29
+ onLowerHand,
30
+ onLowerAll,
31
+ unread,
32
+ fill = false,
33
+ }: {
34
+ tab: SidePanelTab;
35
+ onTab: (tab: SidePanelTab) => void;
36
+ onClose: () => void;
37
+ tokens: StageTokens;
38
+ t: ForgeT;
39
+ chat?: CallStageChat;
40
+ tiles: readonly CallTile[];
41
+ hands: readonly RaisedHand[];
42
+ isHost: boolean;
43
+ myIdentity: string | null;
44
+ onLowerHand: (identity: string) => void;
45
+ onLowerAll: () => void;
46
+ unread: number;
47
+ /** Fill the parent (a full-screen overlay on a phone) instead of the fixed
48
+ * docked width. */
49
+ fill?: boolean;
50
+ }) {
51
+ const tabButton = (which: SidePanelTab, label: string, badge?: number) => (
52
+ <button
53
+ type="button"
54
+ onClick={() => onTab(which)}
55
+ aria-pressed={tab === which}
56
+ style={{
57
+ flex: 1,
58
+ display: "inline-flex",
59
+ alignItems: "center",
60
+ justifyContent: "center",
61
+ gap: "0.375rem",
62
+ padding: "0.5rem",
63
+ border: "none",
64
+ borderBottom: `2px solid ${tab === which ? tokens.accent : "transparent"}`,
65
+ backgroundColor: "transparent",
66
+ color: tab === which ? tokens.stageText : tokens.stageMuted,
67
+ cursor: "pointer",
68
+ font: "inherit",
69
+ fontSize: "0.8125rem",
70
+ fontWeight: 600,
71
+ }}
72
+ >
73
+ {label}
74
+ {!!badge && (
75
+ <span
76
+ style={{
77
+ minWidth: 18,
78
+ height: 18,
79
+ borderRadius: 9999,
80
+ backgroundColor: tokens.accent,
81
+ color: tokens.accentText,
82
+ fontSize: "0.6875rem",
83
+ display: "inline-flex",
84
+ alignItems: "center",
85
+ justifyContent: "center",
86
+ padding: "0 0.25rem",
87
+ }}
88
+ >
89
+ {badge > 99 ? "99+" : badge}
90
+ </span>
91
+ )}
92
+ </button>
93
+ );
94
+
95
+ return (
96
+ <aside
97
+ data-testid="call-side-panel"
98
+ style={{
99
+ width: fill ? "100%" : "min(20rem, 85vw)",
100
+ ...(fill ? { height: "100%" } : {}),
101
+ display: "flex",
102
+ flexDirection: "column",
103
+ minHeight: 0,
104
+ borderRadius: tokens.cornerRadius,
105
+ backgroundColor: tokens.stageSurface,
106
+ border: `1px solid ${tokens.stageBorder}`,
107
+ color: tokens.stageText,
108
+ overflow: "hidden",
109
+ }}
110
+ >
111
+ <div style={{ display: "flex", alignItems: "center", borderBottom: `1px solid ${tokens.stageBorder}` }}>
112
+ {chat && tabButton("chat", t("forge.call_stage.tab_chat"), tab === "chat" ? 0 : unread)}
113
+ {tabButton("people", t("forge.call_stage.tab_people"), hands.length)}
114
+ <button
115
+ type="button"
116
+ onClick={onClose}
117
+ aria-label={t("forge.call_stage.close_panel")}
118
+ style={{
119
+ display: "inline-flex",
120
+ alignItems: "center",
121
+ justifyContent: "center",
122
+ width: 32,
123
+ height: 32,
124
+ margin: "0 0.25rem",
125
+ border: "none",
126
+ borderRadius: tokens.cornerRadius,
127
+ backgroundColor: "transparent",
128
+ color: tokens.stageMuted,
129
+ cursor: "pointer",
130
+ }}
131
+ >
132
+ <X width={16} height={16} aria-hidden="true" />
133
+ </button>
134
+ </div>
135
+
136
+ {tab === "chat" && chat ? (
137
+ <CallChatPanel chat={chat} tokens={tokens} t={t} />
138
+ ) : (
139
+ <CallParticipantsPanel
140
+ tiles={tiles}
141
+ hands={hands}
142
+ isHost={isHost}
143
+ myIdentity={myIdentity}
144
+ onLowerHand={onLowerHand}
145
+ onLowerAll={onLowerAll}
146
+ tokens={tokens}
147
+ t={t}
148
+ />
149
+ )}
150
+ </aside>
151
+ );
152
+ }
@@ -0,0 +1,89 @@
1
+ import { Loader2, PhoneOff, RefreshCw, Users } from "lucide-react";
2
+
3
+ import type { ForgeT } from "../../../i18n";
4
+ import type { StageNotice } from "../callState";
5
+ import type { StageTokens } from "./stageTheme";
6
+
7
+ /**
8
+ * The centre of the stage, when the stage itself is the message.
9
+ *
10
+ * Terminal states (ended, failed) and empty ones (joining, waiting,
11
+ * everyone-left) render HERE, where a person staring at a dead stage actually
12
+ * looks; the top pill keeps the transient states so live tiles stay visible
13
+ * behind them. What renders is `stageNotice`'s one decision, so this file
14
+ * holds no logic worth testing.
15
+ */
16
+ export function CallCenterNotice({
17
+ notice,
18
+ tokens,
19
+ t,
20
+ onRetry,
21
+ }: {
22
+ notice: StageNotice;
23
+ tokens: StageTokens;
24
+ t: ForgeT;
25
+ onRetry: () => void;
26
+ }) {
27
+ const detail = notice.detailKey ? t(notice.detailKey) : notice.detailText;
28
+ const busy = notice.kind === "joining";
29
+ const Icon = notice.kind === "joining" ? Loader2 : notice.kind === "waiting" || notice.kind === "everyone_left" ? Users : PhoneOff;
30
+
31
+ return (
32
+ <div
33
+ // `alert` only for a failure somebody must not miss. The other kinds
34
+ // carry NO live region: the top pill already announces the connection
35
+ // state, and two live regions saying the same thing read twice.
36
+ {...(notice.kind === "failed" ? { role: "alert" } : {})}
37
+ data-testid={`stage-notice-${notice.kind}`}
38
+ style={{
39
+ position: "absolute",
40
+ inset: 0,
41
+ display: "flex",
42
+ flexDirection: "column",
43
+ alignItems: "center",
44
+ justifyContent: "center",
45
+ gap: "0.75rem",
46
+ textAlign: "center",
47
+ padding: "1.5rem",
48
+ color: tokens.stageText,
49
+ // Terminal states cover what is behind them; the empty states have
50
+ // nothing behind them to cover.
51
+ backgroundColor: notice.kind === "ended" || notice.kind === "failed" ? tokens.stageBg : "transparent",
52
+ borderRadius: tokens.cornerRadius,
53
+ }}
54
+ >
55
+ <Icon
56
+ width={28}
57
+ height={28}
58
+ aria-hidden="true"
59
+ color={notice.kind === "failed" ? tokens.danger : tokens.stageMuted}
60
+ style={busy ? { animation: "forge-call-spin 1.2s linear infinite" } : undefined}
61
+ />
62
+ <strong style={{ fontSize: "1.05rem" }}>{t(notice.headlineKey)}</strong>
63
+ {detail && <span style={{ fontSize: "0.875rem", color: tokens.stageMuted, maxWidth: "28rem" }}>{detail}</span>}
64
+ {notice.canRetry && (
65
+ <button
66
+ type="button"
67
+ onClick={onRetry}
68
+ style={{
69
+ display: "inline-flex",
70
+ alignItems: "center",
71
+ gap: "0.375rem",
72
+ padding: "0.5rem 1rem",
73
+ borderRadius: tokens.cornerRadius,
74
+ border: "none",
75
+ cursor: "pointer",
76
+ backgroundColor: tokens.accent,
77
+ color: tokens.accentText,
78
+ font: "inherit",
79
+ fontSize: "0.875rem",
80
+ }}
81
+ >
82
+ <RefreshCw width={14} height={14} aria-hidden="true" />
83
+ {t("forge.call_stage.retry")}
84
+ </button>
85
+ )}
86
+ <style>{`@keyframes forge-call-spin { to { transform: rotate(360deg); } }`}</style>
87
+ </div>
88
+ );
89
+ }