@tribe-nest/forge 3.59.0 → 3.61.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.
- package/package.json +2 -1
- package/src/_tests/testCommunity.tsx +28 -0
- package/src/contexts/CartContext.tsx +34 -1
- package/src/contexts/CommunityContext.tsx +192 -0
- package/src/contexts/PublicAuthContext.tsx +15 -3
- package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
- package/src/data/queries/useAccount.ts +33 -2
- package/src/data/queries/useAnalytics.ts +6 -6
- package/src/data/queries/useCallChat.ts +2 -2
- package/src/data/queries/useChat.ts +52 -40
- package/src/data/queries/useCheckouts.ts +2 -1
- package/src/data/queries/useCoachingAvailability.ts +10 -1
- package/src/data/queries/useCommunities.ts +345 -0
- package/src/data/queries/useCommunity.ts +450 -102
- package/src/data/queries/useCommunityAgent.ts +247 -0
- package/src/data/queries/useCommunityCalendar.ts +150 -0
- package/src/data/queries/useCommunityClassroom.ts +75 -0
- package/src/data/queries/useCommunityMembers.ts +108 -0
- package/src/data/queries/useCommunityModeration.ts +327 -0
- package/src/data/queries/useCommunityWiki.ts +137 -0
- package/src/data/queries/useCourses.ts +2 -0
- package/src/data/queries/useEmailSignIn.ts +79 -0
- package/src/data/queries/useEvents.ts +6 -1
- package/src/data/queries/useForms.ts +5 -1
- package/src/data/queries/useLeadMagnet.ts +6 -2
- package/src/data/queries/useMembership.ts +32 -7
- package/src/data/queries/useMembershipCalls.ts +116 -7
- package/src/data/queries/useMyBookings.ts +24 -1
- package/src/data/queries/useMyTickets.ts +24 -1
- package/src/data/queries/useNotifications.ts +6 -2
- package/src/data/queries/useOrders.ts +2 -1
- package/src/data/queries/usePWA.ts +111 -35
- package/src/data/queries/usePostCollections.ts +28 -16
- package/src/data/queries/usePosts.ts +16 -14
- package/src/data/queries/useSiteApp.ts +130 -0
- package/src/data/queries/useSubscriptions.ts +28 -9
- package/src/i18n/de.json +891 -2
- package/src/i18n/en.json +891 -2
- package/src/index.ts +36 -7
- package/src/runtime/pages.spec.ts +36 -0
- package/src/runtime/pages.ts +52 -27
- package/src/server/_tests/buildWebManifest.spec.ts +1 -1
- package/src/server/_tests/forgeServiceWorkerScript.spec.ts +4 -4
- package/src/types/models.ts +11 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +64 -34
- package/src/ui/analytics/PageMetaPixel.tsx +23 -8
- package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
- package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
- package/src/ui/analytics/useMetaEvents.ts +78 -0
- package/src/ui/format/_tests/membershipGate.spec.ts +6 -6
- package/src/ui/format/membershipGate.ts +19 -15
- package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
- package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
- package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
- package/src/ui/headless/_tests/communityGate.spec.tsx +80 -0
- package/src/ui/headless/_tests/communityHarness.tsx +212 -0
- package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
- package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
- package/src/ui/headless/_tests/communityProvider.spec.tsx +151 -0
- package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
- package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
- package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
- package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
- package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
- package/src/ui/headless/_tests/useCommunityInvite.spec.tsx +104 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +596 -0
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +179 -0
- package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +295 -0
- package/src/ui/headless/_tests/useCommunityUpgrade.spec.tsx +157 -0
- package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
- package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
- package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
- package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
- package/src/ui/headless/auth/useSignupForm.ts +30 -3
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
- package/src/ui/headless/chat/useChatRoom.ts +35 -1
- package/src/ui/headless/communities/CommunityGate.tsx +67 -0
- package/src/ui/headless/communities/index.ts +96 -0
- package/src/ui/headless/communities/paths.ts +36 -0
- package/src/ui/headless/communities/useCommunity.ts +140 -0
- package/src/ui/headless/communities/useCommunityAbout.ts +71 -0
- package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
- package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
- package/src/ui/headless/communities/useCommunityHome.ts +91 -0
- package/src/ui/headless/communities/useCommunityInvite.ts +116 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +494 -0
- package/src/ui/headless/communities/useCommunityMembership.ts +182 -0
- package/src/ui/headless/communities/useCommunityUpgrade.ts +213 -0
- package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
- package/src/ui/headless/communities/useMyMemberships.ts +59 -0
- package/src/ui/headless/community/communityPoll.ts +297 -0
- package/src/ui/headless/community/countries.ts +123 -0
- package/src/ui/headless/community/index.ts +81 -1
- package/src/ui/headless/community/linkPreview.ts +71 -0
- package/src/ui/headless/community/richLexical.ts +27 -2
- package/src/ui/headless/community/useCommunityAgent.ts +285 -0
- package/src/ui/headless/community/useCommunityAttachmentUpload.ts +7 -3
- package/src/ui/headless/community/useCommunityMembers.ts +175 -0
- package/src/ui/headless/community/useCommunityModeration.ts +300 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
- package/src/ui/headless/community/useCommunityThread.ts +16 -3
- package/src/ui/headless/community/useMentionComposer.ts +17 -9
- package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
- package/src/ui/headless/downloads/index.ts +8 -0
- package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
- package/src/ui/headless/index.ts +21 -4
- package/src/ui/headless/memberHome/index.ts +11 -1
- package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +84 -44
- package/src/ui/headless/membership/_tests/membershipCheckoutRefetch.spec.tsx +8 -0
- package/src/ui/headless/membership/_tests/useMembershipCheckoutApplication.spec.tsx +5 -4
- package/src/ui/headless/membership/_tests/useMembershipCheckoutQuestionnaire.spec.tsx +1 -1
- package/src/ui/headless/membership/_tests/useMembershipCheckoutTrial.spec.tsx +1 -1
- package/src/ui/headless/membership/useMembershipCheckout.ts +89 -22
- package/src/ui/headless/membership/useMembershipGateNotice.ts +25 -7
- package/src/ui/headless/portal/index.ts +57 -0
- package/src/ui/headless/portal/usePortal.ts +274 -0
- package/src/ui/headless/portal/usePortalFiles.ts +68 -0
- package/src/ui/headless/portal/usePortalMessages.ts +189 -0
- package/src/ui/headless/portal/usePortalPreview.ts +142 -0
- package/src/ui/headless/posts/upsell.ts +54 -8
- package/src/ui/headless/posts/useMemberPostsFeed.ts +6 -5
- package/src/ui/headless/posts/usePostCollectionDetail.ts +7 -4
- package/src/ui/headless/posts/usePostCollectionsList.ts +6 -5
- package/src/ui/headless/posts/usePostItem.ts +6 -5
- package/src/ui/headless/siteApp/appBar.ts +102 -0
- package/src/ui/headless/siteApp/appHome.ts +344 -0
- package/src/ui/headless/siteApp/appPrompts.ts +229 -0
- package/src/ui/headless/siteApp/index.ts +58 -0
- package/src/ui/headless/siteApp/useAppHome.ts +210 -0
- package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
- package/src/ui/headless/work/index.ts +2 -0
- package/src/ui/headless/work/useWorkPortal.ts +37 -26
- package/src/ui/index.ts +28 -12
- package/src/ui/styled/AccountDashboard.tsx +754 -602
- package/src/ui/styled/AiAgentWidget.tsx +13 -1
- package/src/ui/styled/ChatRoom.tsx +126 -16
- package/src/ui/styled/Checkout.tsx +8 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingDetail.tsx +2 -0
- package/src/ui/styled/CourseConfirmation.tsx +5 -0
- package/src/ui/styled/CourseDetail.tsx +2 -0
- package/src/ui/styled/DownloadPage.tsx +263 -0
- package/src/ui/styled/EventConfirmation.tsx +10 -0
- package/src/ui/styled/EventDetail.tsx +10 -0
- package/src/ui/styled/FilmLibrary.tsx +22 -9
- package/src/ui/styled/MembershipCheckout.tsx +37 -17
- package/src/ui/styled/MembershipTierCallout.tsx +20 -1
- package/src/ui/styled/MembershipTiers.tsx +49 -11
- package/src/ui/styled/MusicLinkPage.tsx +5 -5
- package/src/ui/styled/Paywall.tsx +1 -1
- package/src/ui/styled/ProductDetail.tsx +12 -0
- package/src/ui/styled/PwaRegistration.tsx +5 -1
- package/src/ui/styled/SignupForm.tsx +18 -3
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +6 -6
- package/src/ui/styled/_tests/AccountDashboardMemberships.spec.tsx +151 -0
- package/src/ui/styled/_tests/AccountDashboardRentals.spec.tsx +1 -1
- package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
- package/src/ui/styled/_tests/CourseCallsSection.spec.tsx +7 -1
- package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
- package/src/ui/styled/_tests/MembershipCheckout.spec.tsx +54 -11
- package/src/ui/styled/_tests/membershipTierCallout.spec.tsx +1 -1
- package/src/ui/styled/_tests/membershipTiersCuratedAccess.spec.tsx +1 -1
- package/src/ui/styled/app/AppBar.tsx +298 -0
- package/src/ui/styled/app/AppHome.tsx +1139 -0
- package/src/ui/styled/app/AppHomeMark.tsx +66 -0
- package/src/ui/styled/app/AppPrompts.tsx +464 -0
- package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
- package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
- package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
- package/src/ui/styled/app/index.ts +7 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
- package/src/ui/styled/chat/MessageBubble.tsx +43 -6
- package/src/ui/styled/communities/CommunitiesList.tsx +88 -0
- package/src/ui/styled/communities/CommunityAbout.tsx +145 -0
- package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
- package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
- package/src/ui/styled/communities/CommunityHome.tsx +298 -0
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +544 -0
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +249 -0
- package/src/ui/styled/communities/CommunityPending.tsx +49 -0
- package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
- package/src/ui/styled/communities/CommunityUpgradeSheet.tsx +137 -0
- package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +115 -0
- package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
- package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
- package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
- package/src/ui/styled/communities/_tests/CommunityPages.spec.tsx +102 -0
- package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
- package/src/ui/styled/communities/index.ts +19 -0
- package/src/ui/styled/communities/shared.tsx +177 -0
- package/src/ui/styled/community/CommunityAgent.tsx +286 -0
- package/src/ui/styled/community/CommunityComposer.tsx +266 -15
- package/src/ui/styled/community/CommunityFeed.tsx +50 -1
- package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
- package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
- package/src/ui/styled/community/CommunityMembers.tsx +387 -0
- package/src/ui/styled/community/CommunityModeration.tsx +587 -0
- package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
- package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
- package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
- package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
- package/src/ui/styled/community/CommunityRichText.tsx +177 -32
- package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
- package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
- package/src/ui/styled/community/_tests/CommunityComposer.spec.tsx +4 -1
- package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
- package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
- package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
- package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
- package/src/ui/styled/community/_tests/CommunityPostDetail.spec.tsx +4 -1
- package/src/ui/styled/community/index.ts +37 -2
- package/src/ui/styled/forge-utilities.css +253 -0
- package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
- package/src/ui/styled/members/CallTranscriptViewer.tsx +424 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +41 -54
- package/src/ui/styled/members/MemberHomeLayout.tsx +327 -29
- package/src/ui/styled/members/MemberPostCard.tsx +9 -5
- package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
- package/src/ui/styled/members/PostCollectionDetail.tsx +18 -10
- package/src/ui/styled/members/PostCollectionsGrid.tsx +24 -10
- package/src/ui/styled/members/_tests/MemberCallsCalendar.spec.tsx +167 -8
- package/src/ui/styled/members/callTranscriptText.ts +67 -0
- package/src/ui/styled/members/index.ts +8 -0
- package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
- package/src/ui/styled/portal/PortalActivity.tsx +167 -0
- package/src/ui/styled/portal/PortalBilling.tsx +197 -0
- package/src/ui/styled/portal/PortalFiles.tsx +279 -0
- package/src/ui/styled/portal/PortalHome.tsx +401 -0
- package/src/ui/styled/portal/PortalMessages.tsx +300 -0
- package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
- package/src/ui/styled/portal/PortalProjects.tsx +407 -0
- package/src/ui/styled/portal/PortalShell.tsx +397 -0
- package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
- package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
- package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
- package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
- package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
- package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
- package/src/ui/styled/portal/index.ts +13 -0
- package/src/ui/styled/portal/portalNav.ts +260 -0
- package/src/ui/styled/portal/shared.tsx +206 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
- package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
- package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
- package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
- package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
- package/src/ui/styled/work/WorkReportBody.tsx +223 -248
- package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
- package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
- package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
- package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
- package/src/ui/styled/work/index.ts +1 -1
- package/src/utils/_tests/metaEvents.spec.ts +264 -0
- package/src/utils/_tests/safeRedirect.spec.ts +2 -2
- package/src/utils/landing.ts +19 -6
- package/src/utils/metaEvents.ts +302 -0
- package/src/ui/headless/membership/MembershipGate.tsx +0 -44
- package/src/ui/styled/_tests/AccountDashboardCommunity.spec.tsx +0 -149
- package/src/ui/styled/_tests/AccountDashboardTrial.spec.tsx +0 -105
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { Download, Search, X } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
5
|
+
import { useMeasuredWidth } from "../../media/hooks/useMeasuredWidth";
|
|
6
|
+
import { useMemberCallTranscript, type MemberCallTranscriptSegment } from "../../../data/queries/useMembershipCalls";
|
|
7
|
+
import { Loading } from "../Loading";
|
|
8
|
+
import { activeSegmentIndex, downloadTranscript, formatOffset } from "./callTranscriptText";
|
|
9
|
+
|
|
10
|
+
/** How long after the member scrolls the transcript themselves it stops following playback. */
|
|
11
|
+
const FOLLOW_PAUSE_MS = 4_000;
|
|
12
|
+
/** Below this width the video sits above the transcript instead of beside it. */
|
|
13
|
+
const STACK_BELOW_PX = 860;
|
|
14
|
+
|
|
15
|
+
/** `text` with every case-insensitive match of `term` highlighted. */
|
|
16
|
+
function highlight(text: string, term: string, color: string): ReactNode {
|
|
17
|
+
if (!term) return text;
|
|
18
|
+
const lower = text.toLowerCase();
|
|
19
|
+
const needle = term.toLowerCase();
|
|
20
|
+
const out: ReactNode[] = [];
|
|
21
|
+
let from = 0;
|
|
22
|
+
for (let at = lower.indexOf(needle); at !== -1; at = lower.indexOf(needle, from)) {
|
|
23
|
+
out.push(
|
|
24
|
+
text.slice(from, at),
|
|
25
|
+
<mark key={at} style={{ background: color, color: "inherit", borderRadius: 2 }}>
|
|
26
|
+
{text.slice(at, at + needle.length)}
|
|
27
|
+
</mark>,
|
|
28
|
+
);
|
|
29
|
+
from = at + needle.length;
|
|
30
|
+
}
|
|
31
|
+
out.push(text.slice(from));
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A recorded call's transcript beside its video, full-screen.
|
|
37
|
+
*
|
|
38
|
+
* A viewer rather than a route: members' calls render on hosted `/i/` pages,
|
|
39
|
+
* on code-built sites and in the client app, and an overlay works on all of
|
|
40
|
+
* them without a new page on each. The same layout as the creator's transcript
|
|
41
|
+
* page: the video plays on one side, the line being spoken highlights and
|
|
42
|
+
* follows along, clicking a line seeks, and there is a search and a Summary
|
|
43
|
+
* tab.
|
|
44
|
+
*
|
|
45
|
+
* The video address is signed for sixty seconds on purpose. When playback
|
|
46
|
+
* fails after that, the viewer fetches a fresh one and resumes where the member
|
|
47
|
+
* was.
|
|
48
|
+
*/
|
|
49
|
+
export function CallTranscriptViewer({ recordingId, onClose }: { recordingId: string; onClose: () => void }) {
|
|
50
|
+
const t = useThemeTokens();
|
|
51
|
+
const query = useMemberCallTranscript(recordingId);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const onKey = (event: KeyboardEvent) => event.key === "Escape" && onClose();
|
|
55
|
+
window.addEventListener("keydown", onKey);
|
|
56
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
57
|
+
}, [onClose]);
|
|
58
|
+
|
|
59
|
+
const shell: CSSProperties = {
|
|
60
|
+
position: "fixed",
|
|
61
|
+
inset: 0,
|
|
62
|
+
zIndex: 1001,
|
|
63
|
+
display: "flex",
|
|
64
|
+
flexDirection: "column",
|
|
65
|
+
background: t.background,
|
|
66
|
+
color: t.text,
|
|
67
|
+
fontFamily: t.fontFamily,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (query.isLoading) {
|
|
71
|
+
return (
|
|
72
|
+
<div role="dialog" aria-modal="true" aria-label="Transcript" style={shell}>
|
|
73
|
+
<Loading />
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (query.error || !query.data) {
|
|
78
|
+
return (
|
|
79
|
+
<div role="dialog" aria-modal="true" aria-label="Transcript" style={{ ...shell, padding: 24, gap: 12 }}>
|
|
80
|
+
<p style={{ margin: 0, color: t.muted }}>We could not open this transcript just now.</p>
|
|
81
|
+
<button type="button" onClick={onClose} style={buttonStyle(t)}>
|
|
82
|
+
Close
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return <ViewerBody detail={query.data} refetch={query.refetch} onClose={onClose} shell={shell} />;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type Tokens = ReturnType<typeof useThemeTokens>;
|
|
92
|
+
|
|
93
|
+
function buttonStyle(t: Tokens): CSSProperties {
|
|
94
|
+
return {
|
|
95
|
+
display: "inline-flex",
|
|
96
|
+
alignItems: "center",
|
|
97
|
+
gap: 6,
|
|
98
|
+
padding: "8px 14px",
|
|
99
|
+
borderRadius: t.cornerRadius,
|
|
100
|
+
border: `1px solid ${t.border}`,
|
|
101
|
+
background: "transparent",
|
|
102
|
+
color: t.text,
|
|
103
|
+
fontFamily: t.fontFamily,
|
|
104
|
+
fontSize: 14,
|
|
105
|
+
fontWeight: 600,
|
|
106
|
+
cursor: "pointer",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ViewerBody({
|
|
111
|
+
detail,
|
|
112
|
+
refetch,
|
|
113
|
+
onClose,
|
|
114
|
+
shell,
|
|
115
|
+
}: {
|
|
116
|
+
detail: NonNullable<ReturnType<typeof useMemberCallTranscript>["data"]>;
|
|
117
|
+
refetch: ReturnType<typeof useMemberCallTranscript>["refetch"];
|
|
118
|
+
onClose: () => void;
|
|
119
|
+
shell: CSSProperties;
|
|
120
|
+
}) {
|
|
121
|
+
const t = useThemeTokens();
|
|
122
|
+
const { segments, summary } = detail.transcript;
|
|
123
|
+
const [bodyRef, width] = useMeasuredWidth<HTMLDivElement>();
|
|
124
|
+
const stacked = width > 0 && width < STACK_BELOW_PX;
|
|
125
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
126
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
127
|
+
const [watchUrl, setWatchUrl] = useState(detail.watchUrl);
|
|
128
|
+
const [currentMs, setCurrentMs] = useState(0);
|
|
129
|
+
const [term, setTerm] = useState("");
|
|
130
|
+
const [tab, setTab] = useState<"summary" | "transcript">(summary ? "summary" : "transcript");
|
|
131
|
+
const lastManualScroll = useRef(0);
|
|
132
|
+
const resume = useRef<{ atSec: number; play: boolean } | null>(null);
|
|
133
|
+
const lastRefresh = useRef(0);
|
|
134
|
+
|
|
135
|
+
const active = activeSegmentIndex(segments, currentMs);
|
|
136
|
+
const needle = term.trim();
|
|
137
|
+
const matches = useMemo(() => {
|
|
138
|
+
const lower = needle.toLowerCase();
|
|
139
|
+
if (!lower) return null;
|
|
140
|
+
return segments
|
|
141
|
+
.map((segment, index) => ({ segment, index }))
|
|
142
|
+
.filter(({ segment }) => `${segment.name} ${segment.text}`.toLowerCase().includes(lower));
|
|
143
|
+
}, [segments, needle]);
|
|
144
|
+
const shown = matches ?? segments.map((segment, index) => ({ segment, index }));
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (tab !== "transcript" || matches || active < 0) return;
|
|
148
|
+
if (Date.now() - lastManualScroll.current < FOLLOW_PAUSE_MS) return;
|
|
149
|
+
listRef.current
|
|
150
|
+
?.querySelector<HTMLElement>(`[data-index="${active}"]`)
|
|
151
|
+
?.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
|
|
152
|
+
}, [active, matches, tab]);
|
|
153
|
+
|
|
154
|
+
const recover = async () => {
|
|
155
|
+
const video = videoRef.current;
|
|
156
|
+
if (!video || Date.now() - lastRefresh.current < 5_000) return;
|
|
157
|
+
lastRefresh.current = Date.now();
|
|
158
|
+
resume.current = { atSec: video.currentTime, play: !video.paused || video.seeking };
|
|
159
|
+
const fresh = await refetch();
|
|
160
|
+
if (fresh.data?.watchUrl) setWatchUrl(fresh.data.watchUrl);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const seek = (segment: MemberCallTranscriptSegment) => {
|
|
164
|
+
const video = videoRef.current;
|
|
165
|
+
if (!video) return;
|
|
166
|
+
video.currentTime = segment.startMs / 1000;
|
|
167
|
+
setCurrentMs(segment.startMs);
|
|
168
|
+
void video.play()?.catch(() => undefined);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const tabStyle = (selected: boolean): CSSProperties => ({
|
|
172
|
+
padding: "6px 12px",
|
|
173
|
+
border: "none",
|
|
174
|
+
borderBottom: `2px solid ${selected ? t.primary : "transparent"}`,
|
|
175
|
+
background: "transparent",
|
|
176
|
+
color: selected ? t.text : t.muted,
|
|
177
|
+
fontFamily: t.fontFamily,
|
|
178
|
+
fontSize: 14,
|
|
179
|
+
fontWeight: 600,
|
|
180
|
+
cursor: "pointer",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div role="dialog" aria-modal="true" aria-label={`Transcript: ${detail.title}`} style={shell}>
|
|
185
|
+
<header
|
|
186
|
+
style={{
|
|
187
|
+
display: "flex",
|
|
188
|
+
alignItems: "center",
|
|
189
|
+
gap: 12,
|
|
190
|
+
padding: "12px 16px",
|
|
191
|
+
borderBottom: `1px solid ${t.border}`,
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
<div style={{ minWidth: 0, flex: 1 }}>
|
|
195
|
+
<h2
|
|
196
|
+
style={{
|
|
197
|
+
margin: 0,
|
|
198
|
+
fontSize: 17,
|
|
199
|
+
fontWeight: 700,
|
|
200
|
+
whiteSpace: "nowrap",
|
|
201
|
+
overflow: "hidden",
|
|
202
|
+
textOverflow: "ellipsis",
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
{detail.title}
|
|
206
|
+
</h2>
|
|
207
|
+
<span style={{ fontSize: 13, color: t.muted }}>
|
|
208
|
+
{new Date(detail.startsAt).toLocaleString("en-US", {
|
|
209
|
+
dateStyle: "long",
|
|
210
|
+
timeStyle: "short",
|
|
211
|
+
timeZone: detail.timezone || undefined,
|
|
212
|
+
})}
|
|
213
|
+
</span>
|
|
214
|
+
</div>
|
|
215
|
+
<button type="button" onClick={() => downloadTranscript(detail)} style={buttonStyle(t)}>
|
|
216
|
+
<Download size={15} />
|
|
217
|
+
Download
|
|
218
|
+
</button>
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
onClick={onClose}
|
|
222
|
+
aria-label="Close"
|
|
223
|
+
style={{ background: "transparent", border: "none", cursor: "pointer", color: t.muted, padding: 4 }}
|
|
224
|
+
>
|
|
225
|
+
<X size={20} />
|
|
226
|
+
</button>
|
|
227
|
+
</header>
|
|
228
|
+
|
|
229
|
+
<div
|
|
230
|
+
ref={bodyRef}
|
|
231
|
+
style={{
|
|
232
|
+
flex: 1,
|
|
233
|
+
minHeight: 0,
|
|
234
|
+
display: "flex",
|
|
235
|
+
flexDirection: stacked ? "column" : "row",
|
|
236
|
+
gap: 16,
|
|
237
|
+
padding: 16,
|
|
238
|
+
overflow: stacked ? "auto" : "hidden",
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
<div style={{ flex: stacked ? "none" : 3, minWidth: 0 }}>
|
|
242
|
+
{watchUrl ? (
|
|
243
|
+
<video
|
|
244
|
+
ref={videoRef}
|
|
245
|
+
src={watchUrl}
|
|
246
|
+
controls
|
|
247
|
+
preload="metadata"
|
|
248
|
+
data-testid="member-transcript-video"
|
|
249
|
+
style={{ width: "100%", aspectRatio: "16 / 9", background: "#000", borderRadius: t.cornerRadius }}
|
|
250
|
+
onTimeUpdate={(event) => setCurrentMs(event.currentTarget.currentTime * 1000)}
|
|
251
|
+
onError={() => void recover()}
|
|
252
|
+
onLoadedMetadata={(event) => {
|
|
253
|
+
const pending = resume.current;
|
|
254
|
+
if (!pending) return;
|
|
255
|
+
resume.current = null;
|
|
256
|
+
event.currentTarget.currentTime = pending.atSec;
|
|
257
|
+
if (pending.play) void event.currentTarget.play()?.catch(() => undefined);
|
|
258
|
+
}}
|
|
259
|
+
/>
|
|
260
|
+
) : (
|
|
261
|
+
<div
|
|
262
|
+
style={{
|
|
263
|
+
width: "100%",
|
|
264
|
+
aspectRatio: "16 / 9",
|
|
265
|
+
display: "flex",
|
|
266
|
+
alignItems: "center",
|
|
267
|
+
justifyContent: "center",
|
|
268
|
+
border: `1px solid ${t.border}`,
|
|
269
|
+
borderRadius: t.cornerRadius,
|
|
270
|
+
color: t.muted,
|
|
271
|
+
fontSize: 14,
|
|
272
|
+
}}
|
|
273
|
+
>
|
|
274
|
+
The video is not available.
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div
|
|
280
|
+
style={{
|
|
281
|
+
flex: stacked ? "none" : 2,
|
|
282
|
+
minWidth: 0,
|
|
283
|
+
minHeight: 0,
|
|
284
|
+
display: "flex",
|
|
285
|
+
flexDirection: "column",
|
|
286
|
+
gap: 10,
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
<div role="tablist" style={{ display: "flex", gap: 4, borderBottom: `1px solid ${t.border}` }}>
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
role="tab"
|
|
293
|
+
aria-selected={tab === "summary"}
|
|
294
|
+
onClick={() => setTab("summary")}
|
|
295
|
+
style={tabStyle(tab === "summary")}
|
|
296
|
+
>
|
|
297
|
+
Summary
|
|
298
|
+
</button>
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
role="tab"
|
|
302
|
+
aria-selected={tab === "transcript"}
|
|
303
|
+
onClick={() => setTab("transcript")}
|
|
304
|
+
style={tabStyle(tab === "transcript")}
|
|
305
|
+
>
|
|
306
|
+
Transcript
|
|
307
|
+
</button>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{tab === "summary" ? (
|
|
311
|
+
<div
|
|
312
|
+
style={{
|
|
313
|
+
display: "flex",
|
|
314
|
+
flexDirection: "column",
|
|
315
|
+
gap: 12,
|
|
316
|
+
fontSize: 14,
|
|
317
|
+
lineHeight: 1.5,
|
|
318
|
+
overflowY: "auto",
|
|
319
|
+
}}
|
|
320
|
+
>
|
|
321
|
+
{summary ? (
|
|
322
|
+
<>
|
|
323
|
+
<p style={{ margin: 0 }}>{summary.summary}</p>
|
|
324
|
+
{[
|
|
325
|
+
["Decisions", summary.decisions],
|
|
326
|
+
["Action items", summary.actionItems],
|
|
327
|
+
].map(
|
|
328
|
+
([label, items]) =>
|
|
329
|
+
(items as string[]).length > 0 && (
|
|
330
|
+
<div key={label as string}>
|
|
331
|
+
<span style={{ fontWeight: 600 }}>{label as string}</span>
|
|
332
|
+
<ul style={{ margin: "4px 0 0", paddingLeft: 20 }}>
|
|
333
|
+
{(items as string[]).map((item, i) => (
|
|
334
|
+
<li key={i}>{item}</li>
|
|
335
|
+
))}
|
|
336
|
+
</ul>
|
|
337
|
+
</div>
|
|
338
|
+
),
|
|
339
|
+
)}
|
|
340
|
+
</>
|
|
341
|
+
) : (
|
|
342
|
+
<p style={{ margin: 0, color: t.muted }}>There is no summary for this call.</p>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
) : (
|
|
346
|
+
<>
|
|
347
|
+
<label style={{ position: "relative", display: "block" }}>
|
|
348
|
+
<Search size={15} style={{ position: "absolute", left: 10, top: 10, color: t.muted }} />
|
|
349
|
+
<input
|
|
350
|
+
value={term}
|
|
351
|
+
onChange={(event) => setTerm(event.target.value)}
|
|
352
|
+
placeholder="Search the transcript"
|
|
353
|
+
aria-label="Search the transcript"
|
|
354
|
+
style={{
|
|
355
|
+
width: "100%",
|
|
356
|
+
boxSizing: "border-box",
|
|
357
|
+
padding: "8px 10px 8px 32px",
|
|
358
|
+
borderRadius: t.cornerRadius,
|
|
359
|
+
border: `1px solid ${t.border}`,
|
|
360
|
+
background: t.surface,
|
|
361
|
+
color: t.text,
|
|
362
|
+
fontFamily: t.fontFamily,
|
|
363
|
+
fontSize: 14,
|
|
364
|
+
}}
|
|
365
|
+
/>
|
|
366
|
+
</label>
|
|
367
|
+
{matches && (
|
|
368
|
+
<span role="status" style={{ fontSize: 12, color: t.muted }}>
|
|
369
|
+
{matches.length === 1 ? "1 match" : `${matches.length} matches`}
|
|
370
|
+
</span>
|
|
371
|
+
)}
|
|
372
|
+
<div
|
|
373
|
+
ref={listRef}
|
|
374
|
+
data-testid="member-transcript-lines"
|
|
375
|
+
onWheel={() => (lastManualScroll.current = Date.now())}
|
|
376
|
+
onTouchMove={() => (lastManualScroll.current = Date.now())}
|
|
377
|
+
style={{
|
|
378
|
+
flex: 1,
|
|
379
|
+
minHeight: stacked ? 320 : 0,
|
|
380
|
+
maxHeight: stacked ? 480 : undefined,
|
|
381
|
+
overflowY: "auto",
|
|
382
|
+
border: `1px solid ${t.border}`,
|
|
383
|
+
borderRadius: t.cornerRadius,
|
|
384
|
+
}}
|
|
385
|
+
>
|
|
386
|
+
{shown.map(({ segment, index }) => (
|
|
387
|
+
<button
|
|
388
|
+
type="button"
|
|
389
|
+
key={index}
|
|
390
|
+
data-index={index}
|
|
391
|
+
aria-current={index === active ? "true" : undefined}
|
|
392
|
+
onClick={() => seek(segment)}
|
|
393
|
+
style={{
|
|
394
|
+
display: "flex",
|
|
395
|
+
gap: 10,
|
|
396
|
+
width: "100%",
|
|
397
|
+
padding: "8px 10px",
|
|
398
|
+
border: "none",
|
|
399
|
+
textAlign: "left",
|
|
400
|
+
background: index === active ? t.surface : "transparent",
|
|
401
|
+
color: t.text,
|
|
402
|
+
fontFamily: t.fontFamily,
|
|
403
|
+
fontSize: 14,
|
|
404
|
+
lineHeight: 1.5,
|
|
405
|
+
cursor: "pointer",
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
<span style={{ width: 56, flexShrink: 0, fontFamily: "monospace", fontSize: 12, color: t.primary }}>
|
|
409
|
+
{formatOffset(segment.startMs)}
|
|
410
|
+
</span>
|
|
411
|
+
<span style={{ minWidth: 0 }}>
|
|
412
|
+
<span style={{ fontWeight: 600 }}>{highlight(segment.name, needle, t.border)}</span>{" "}
|
|
413
|
+
{highlight(segment.text, needle, t.border)}
|
|
414
|
+
</span>
|
|
415
|
+
</button>
|
|
416
|
+
))}
|
|
417
|
+
</div>
|
|
418
|
+
</>
|
|
419
|
+
)}
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import { useState, type CSSProperties, type ReactNode } from "react";
|
|
2
|
-
import {
|
|
3
|
-
CalendarDays,
|
|
4
|
-
ChevronLeft,
|
|
5
|
-
ChevronRight,
|
|
6
|
-
FileText,
|
|
7
|
-
Loader2,
|
|
8
|
-
MessageSquare,
|
|
9
|
-
Mic,
|
|
10
|
-
MicOff,
|
|
11
|
-
PlayCircle,
|
|
12
|
-
Users,
|
|
13
|
-
X,
|
|
14
|
-
} from "lucide-react";
|
|
2
|
+
import { CalendarDays, ChevronLeft, ChevronRight, MessageSquare, Mic, MicOff, Users, X } from "lucide-react";
|
|
15
3
|
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
16
4
|
import { useMemberCalls, type UseMemberCallsOptions } from "../../headless/memberHome/useMemberCalls";
|
|
17
5
|
import type { MyMembershipCall } from "../../../data/queries/useMembershipCalls";
|
|
6
|
+
import { CallTranscriptViewer } from "./CallTranscriptViewer";
|
|
7
|
+
import {
|
|
8
|
+
CallRecordingProcessing,
|
|
9
|
+
CallRecordingWatchLink,
|
|
10
|
+
CallTranscriptPanel,
|
|
11
|
+
callRecordingWatchable,
|
|
12
|
+
} from "./CallRecordingActions";
|
|
18
13
|
import { Loading } from "../Loading";
|
|
19
14
|
|
|
20
15
|
export interface MemberCallsCalendarProps extends UseMemberCallsOptions {
|
|
@@ -119,6 +114,8 @@ export function MemberCallsCalendar({
|
|
|
119
114
|
// The call whose details modal is open. Clicking an event opens it, the way
|
|
120
115
|
// Google Calendar opens an event rather than jumping straight into it.
|
|
121
116
|
const [selected, setSelected] = useState<MyMembershipCall | null>(null);
|
|
117
|
+
// The recording whose transcript is open full-screen, beside its video.
|
|
118
|
+
const [readingRecordingId, setReadingRecordingId] = useState<string | null>(null);
|
|
122
119
|
|
|
123
120
|
if (calendar.isLoading) return <Loading />;
|
|
124
121
|
|
|
@@ -302,13 +299,23 @@ export function MemberCallsCalendar({
|
|
|
302
299
|
|
|
303
300
|
{selected && (
|
|
304
301
|
<CallDetailsModal
|
|
305
|
-
|
|
302
|
+
// Re-read from the latest fetch, not the snapshot taken on click, so
|
|
303
|
+
// a transcript that finishes while the modal is open appears in it.
|
|
304
|
+
call={calendar.calls.find((call) => call.id === selected.id) ?? selected}
|
|
306
305
|
now={now}
|
|
307
306
|
tokens={t}
|
|
308
307
|
onClose={() => setSelected(null)}
|
|
309
308
|
onJoin={() => selected.callPublicId && go(getCallHref(selected.callPublicId))}
|
|
309
|
+
onReadTranscript={(recordingId) => {
|
|
310
|
+
setSelected(null);
|
|
311
|
+
setReadingRecordingId(recordingId);
|
|
312
|
+
}}
|
|
310
313
|
/>
|
|
311
314
|
)}
|
|
315
|
+
|
|
316
|
+
{readingRecordingId && (
|
|
317
|
+
<CallTranscriptViewer recordingId={readingRecordingId} onClose={() => setReadingRecordingId(null)} />
|
|
318
|
+
)}
|
|
312
319
|
</div>
|
|
313
320
|
);
|
|
314
321
|
}
|
|
@@ -370,27 +377,29 @@ function EmptyMonth({
|
|
|
370
377
|
* particulars and everything you can DO with it, without leaving the month.
|
|
371
378
|
*
|
|
372
379
|
* This is where the actions moved when the chronological list was removed:
|
|
373
|
-
* Join while the room is open, Watch once the recording is ready
|
|
374
|
-
* the
|
|
375
|
-
*
|
|
376
|
-
*
|
|
380
|
+
* Join while the room is open, Watch once the recording is ready, and read
|
|
381
|
+
* the transcript and summary once they are written. The chat history of the
|
|
382
|
+
* call is still to come, and is shown as a clearly-labelled placeholder rather
|
|
383
|
+
* than hidden, so the shape a member will grow into is already visible.
|
|
377
384
|
*/
|
|
378
|
-
function CallDetailsModal({
|
|
385
|
+
export function CallDetailsModal({
|
|
379
386
|
call,
|
|
380
387
|
now,
|
|
381
388
|
tokens: t,
|
|
382
389
|
onClose,
|
|
383
390
|
onJoin,
|
|
391
|
+
onReadTranscript,
|
|
384
392
|
}: {
|
|
385
393
|
call: MyMembershipCall;
|
|
386
394
|
now: Date;
|
|
387
395
|
tokens: ReturnType<typeof useThemeTokens>;
|
|
388
396
|
onClose: () => void;
|
|
389
397
|
onJoin: () => void;
|
|
398
|
+
onReadTranscript: (recordingId: string) => void;
|
|
390
399
|
}) {
|
|
391
400
|
const canJoin = call.isOpen && !!call.callPublicId;
|
|
392
401
|
const hasEnded = new Date(call.closesAt).getTime() < now.getTime();
|
|
393
|
-
const watchable = call.recording
|
|
402
|
+
const watchable = callRecordingWatchable(call.recording);
|
|
394
403
|
const processing = call.recording?.status === "processing";
|
|
395
404
|
|
|
396
405
|
const row: CSSProperties = { display: "flex", alignItems: "center", gap: 8, fontSize: 14, color: t.muted };
|
|
@@ -539,36 +548,8 @@ function CallDetailsModal({
|
|
|
539
548
|
Join call
|
|
540
549
|
</button>
|
|
541
550
|
)}
|
|
542
|
-
{watchable &&
|
|
543
|
-
|
|
544
|
-
href={call.recording!.watchUrl!}
|
|
545
|
-
target="_blank"
|
|
546
|
-
rel="noreferrer"
|
|
547
|
-
style={{
|
|
548
|
-
display: "flex",
|
|
549
|
-
alignItems: "center",
|
|
550
|
-
gap: 6,
|
|
551
|
-
padding: "10px 20px",
|
|
552
|
-
borderRadius: t.cornerRadius,
|
|
553
|
-
background: canJoin ? "transparent" : t.primary,
|
|
554
|
-
border: canJoin ? `1px solid ${t.border}` : "none",
|
|
555
|
-
color: canJoin ? t.text : t.textPrimary,
|
|
556
|
-
fontSize: 14,
|
|
557
|
-
fontWeight: 700,
|
|
558
|
-
textDecoration: "none",
|
|
559
|
-
}}
|
|
560
|
-
>
|
|
561
|
-
<PlayCircle size={16} />
|
|
562
|
-
Watch recording
|
|
563
|
-
{call.recording!.durationSeconds ? ` · ${Math.round(call.recording!.durationSeconds / 60)} min` : ""}
|
|
564
|
-
</a>
|
|
565
|
-
)}
|
|
566
|
-
{!canJoin && !watchable && processing && (
|
|
567
|
-
<span style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 14, color: t.muted, fontWeight: 600 }}>
|
|
568
|
-
<Loader2 size={15} />
|
|
569
|
-
Recording is processing
|
|
570
|
-
</span>
|
|
571
|
-
)}
|
|
551
|
+
{watchable && <CallRecordingWatchLink recording={call.recording!} secondary={canJoin} />}
|
|
552
|
+
{!canJoin && !watchable && processing && <CallRecordingProcessing />}
|
|
572
553
|
{!canJoin && !watchable && !processing && (
|
|
573
554
|
<span style={{ fontSize: 14, color: t.muted, fontWeight: 600 }}>
|
|
574
555
|
{hasEnded ? "This call has ended" : opensIn(call.opensAt, now)}
|
|
@@ -576,11 +557,17 @@ function CallDetailsModal({
|
|
|
576
557
|
)}
|
|
577
558
|
</div>
|
|
578
559
|
|
|
579
|
-
{
|
|
580
|
-
|
|
581
|
-
|
|
560
|
+
{call.recording?.transcript && (
|
|
561
|
+
<CallTranscriptPanel
|
|
562
|
+
recordingId={call.recording.id}
|
|
563
|
+
transcript={call.recording.transcript}
|
|
564
|
+
onRead={() => onReadTranscript(call.recording!.id)}
|
|
565
|
+
/>
|
|
566
|
+
)}
|
|
567
|
+
|
|
568
|
+
{/* What is coming: the chat history of the call. Shown so a member knows
|
|
569
|
+
the modal is where it will live, not hidden until the day it ships. */}
|
|
582
570
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
583
|
-
{comingSoon(<FileText size={16} />, "Transcript", "A searchable transcript of what was said.")}
|
|
584
571
|
{comingSoon(<MessageSquare size={16} />, "Chat history", "The messages sent during the call.")}
|
|
585
572
|
</div>
|
|
586
573
|
</div>
|