@tribe-nest/forge 3.42.1 → 3.50.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 (49) 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 +212 -0
  5. package/src/i18n/de.json +74 -1
  6. package/src/i18n/en.json +74 -1
  7. package/src/index.ts +7 -0
  8. package/src/types/models.ts +58 -5
  9. package/src/ui/headless/memberHome/index.ts +4 -0
  10. package/src/ui/headless/memberHome/useMemberCalls.ts +150 -0
  11. package/src/ui/headless/memberHome/useMemberHomeNav.ts +29 -5
  12. package/src/ui/index.ts +3 -0
  13. package/src/ui/media/CallStage.tsx +691 -553
  14. package/src/ui/media/PreJoin.tsx +269 -0
  15. package/src/ui/media/_tests/CallStage.spec.tsx +338 -5
  16. package/src/ui/media/_tests/callLayout.spec.ts +182 -0
  17. package/src/ui/media/_tests/callSignals.spec.ts +87 -0
  18. package/src/ui/media/_tests/callState.spec.ts +93 -1
  19. package/src/ui/media/_tests/fakeNode.ts +4 -1
  20. package/src/ui/media/callChat.ts +33 -0
  21. package/src/ui/media/callState.ts +125 -9
  22. package/src/ui/media/hooks/useCallSignals.ts +135 -0
  23. package/src/ui/media/hooks/useMeasuredWidth.ts +39 -0
  24. package/src/ui/media/index.ts +41 -1
  25. package/src/ui/media/panels/CallChatPanel.tsx +200 -0
  26. package/src/ui/media/panels/CallParticipantsPanel.tsx +125 -0
  27. package/src/ui/media/panels/CallSidePanel.tsx +152 -0
  28. package/src/ui/media/stage/CallCenterNotice.tsx +89 -0
  29. package/src/ui/media/stage/CallControlsBar.tsx +547 -0
  30. package/src/ui/media/stage/CallHealthPanel.tsx +132 -0
  31. package/src/ui/media/stage/CallToasts.tsx +49 -0
  32. package/src/ui/media/stage/CallTopBar.tsx +257 -0
  33. package/src/ui/media/stage/DeviceMenu.tsx +142 -0
  34. package/src/ui/media/stage/ReactionOverlay.tsx +38 -0
  35. package/src/ui/media/stage/SelfView.tsx +146 -0
  36. package/src/ui/media/stage/VideoTile.tsx +232 -0
  37. package/src/ui/media/stage/callLayout.ts +253 -0
  38. package/src/ui/media/stage/callSignals.ts +91 -0
  39. package/src/ui/media/stage/callSounds.ts +78 -0
  40. package/src/ui/media/stage/stageTheme.ts +50 -0
  41. package/src/ui/styled/CohortPage.tsx +15 -1
  42. package/src/ui/styled/CourseAccess.tsx +19 -6
  43. package/src/ui/styled/CourseCallsSection.tsx +99 -0
  44. package/src/ui/styled/FormRenderer.tsx +14 -4
  45. package/src/ui/styled/_tests/CourseCallsSection.spec.tsx +163 -0
  46. package/src/ui/styled/forge-utilities.css +13 -0
  47. package/src/ui/styled/members/MemberCallsCalendar.tsx +589 -0
  48. package/src/ui/styled/members/_tests/MemberCallsCalendar.spec.tsx +135 -0
  49. package/src/ui/styled/members/index.ts +1 -0
@@ -0,0 +1,135 @@
1
+ // @vitest-environment jsdom
2
+ import { cleanup, fireEvent, render, waitFor } from "@testing-library/react";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { ForgeThemeProvider } from "../../../theme/ForgeThemeProvider";
6
+ import type { MyMembershipCall } from "../../../../data/queries/useMembershipCalls";
7
+
8
+ /** The calendar reads its grid from this headless hook; mock it so the test is
9
+ * about the CALENDAR (grid, modal, actions), not the query stack. */
10
+ const calls: MyMembershipCall[] = [];
11
+ vi.mock("../../../headless/memberHome/useMemberCalls", () => ({
12
+ useMemberCalls: () => ({
13
+ isLoading: false,
14
+ error: null,
15
+ monthLabel: "March 2026",
16
+ isCurrentMonth: true,
17
+ goToPreviousMonth: () => {},
18
+ goToNextMonth: () => {},
19
+ goToToday: () => {},
20
+ calls,
21
+ days: [
22
+ {
23
+ key: "2026-03-10",
24
+ date: new Date("2026-03-10T12:00:00Z"),
25
+ isToday: true,
26
+ inMonth: true,
27
+ calls,
28
+ },
29
+ ],
30
+ }),
31
+ }));
32
+
33
+ import { MemberCallsCalendar } from "../MemberCallsCalendar";
34
+
35
+ const call = (over: Partial<MyMembershipCall> = {}): MyMembershipCall => ({
36
+ id: "call-1",
37
+ title: "Weekly Q&A",
38
+ description: "Bring your questions.",
39
+ startsAt: "2026-03-10T17:00:00.000Z",
40
+ endsAt: "2026-03-10T18:00:00.000Z",
41
+ timezone: "America/New_York",
42
+ participation: "open",
43
+ role: "attendee",
44
+ audiences: [{ id: "a1", audienceType: "all_members", audienceId: null, label: "All members" }],
45
+ opensAt: "2026-03-10T16:45:00.000Z",
46
+ closesAt: "2026-03-10T20:00:00.000Z",
47
+ isOpen: false,
48
+ callPublicId: "pub-1",
49
+ recording: null,
50
+ ...over,
51
+ });
52
+
53
+ const mount = (navigate = vi.fn()) =>
54
+ render(
55
+ <ForgeThemeProvider theme={{ colors: { text: "#111", background: "#fff", primary: "#6d28d9" }, cornerRadius: 8 }}>
56
+ <MemberCallsCalendar navigate={navigate} now={new Date("2026-03-10T12:00:00Z")} />
57
+ </ForgeThemeProvider>,
58
+ );
59
+
60
+ afterEach(() => {
61
+ calls.length = 0;
62
+ cleanup();
63
+ });
64
+
65
+ describe("MemberCallsCalendar", () => {
66
+ it("shows NO chronological list under the grid: the calendar is the surface", () => {
67
+ calls.push(call());
68
+ const { queryByText, getAllByText } = mount();
69
+ // The old list row carried the description as body text and a "Join"/status
70
+ // cluster inline. With the list gone, none of that renders until an event
71
+ // is clicked. The title shows (grid on desktop, agenda on mobile - both
72
+ // are in the DOM under test), the description does not.
73
+ expect(queryByText("Bring your questions.")).toBeNull();
74
+ expect(getAllByText("Weekly Q&A").length).toBeGreaterThan(0);
75
+ });
76
+
77
+ it("renders BOTH faces so a phone is not stuck with the grid: a titled grid cell and an agenda row", () => {
78
+ calls.push(call());
79
+ const { getByTitle, getAllByText } = mount();
80
+ // The grid cell carries a title attribute; the agenda row does not, so the
81
+ // title text appears twice (one per face) but only one is titled.
82
+ expect(getByTitle("Weekly Q&A")).toBeTruthy();
83
+ expect(getAllByText("Weekly Q&A").length).toBe(2);
84
+ });
85
+
86
+ it("opens a details modal when an event is clicked, Google-Calendar style", () => {
87
+ calls.push(call({ isOpen: true }));
88
+ const navigate = vi.fn();
89
+ const { getByTitle, getByText, getByRole } = mount(navigate);
90
+
91
+ fireEvent.click(getByTitle("Weekly Q&A"));
92
+
93
+ const dialog = getByRole("dialog");
94
+ expect(dialog).toBeTruthy();
95
+ // The details the list used to show now live in the modal.
96
+ expect(dialog.textContent).toContain("Bring your questions.");
97
+ expect(dialog.textContent).toContain("All members");
98
+ // Recording, transcript and chat are the modal's remit; the two future
99
+ // ones are present as clearly-labelled placeholders.
100
+ expect(dialog.textContent).toContain("Transcript");
101
+ expect(dialog.textContent).toContain("Chat history");
102
+ expect(dialog.textContent).toContain("Coming soon");
103
+
104
+ // The call is open, so Join is offered and routes to the call page.
105
+ fireEvent.click(getByText("Join call"));
106
+ expect(navigate).toHaveBeenCalledWith("/i/video-calls/pub-1");
107
+ });
108
+
109
+ it("offers Watch in the modal when the recording is ready, and no Join once closed", () => {
110
+ calls.push(
111
+ call({
112
+ isOpen: false,
113
+ closesAt: "2026-03-09T20:00:00.000Z",
114
+ recording: { id: "r1", status: "ready", durationSeconds: 1800, watchUrl: "https://cdn.test/rec.mp4" },
115
+ }),
116
+ );
117
+ const { getByTitle, getByText, getByRole, queryByText } = mount();
118
+ fireEvent.click(getByTitle("Weekly Q&A"));
119
+ const dialog = getByRole("dialog");
120
+
121
+ expect(queryByText("Join call")).toBeNull();
122
+ const watch = getByText(/Watch recording/);
123
+ expect(watch.getAttribute("href")).toBe("https://cdn.test/rec.mp4");
124
+ expect(dialog.textContent).toContain("30 min");
125
+ });
126
+
127
+ it("closes on the backdrop", () => {
128
+ calls.push(call());
129
+ const { getByTitle, getByRole, queryByRole } = mount();
130
+ fireEvent.click(getByTitle("Weekly Q&A"));
131
+ const dialog = getByRole("dialog");
132
+ fireEvent.click(dialog);
133
+ expect(queryByRole("dialog")).toBeNull();
134
+ });
135
+ });
@@ -5,3 +5,4 @@ export { MemberPostsFeed, type MemberPostsFeedProps } from "./MemberPostsFeed";
5
5
  export { MemberPostCard, type MemberPostCardProps } from "./MemberPostCard";
6
6
  export { PostCollectionsGrid, type PostCollectionsGridProps } from "./PostCollectionsGrid";
7
7
  export { PostCollectionDetail, type PostCollectionDetailProps } from "./PostCollectionDetail";
8
+ export { MemberCallsCalendar, type MemberCallsCalendarProps } from "./MemberCallsCalendar";