@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
|
@@ -6,12 +6,13 @@ import type {
|
|
|
6
6
|
QuestionnaireAnswer,
|
|
7
7
|
} from "../../types/models";
|
|
8
8
|
import { useForge } from "../../provider/ForgeProvider";
|
|
9
|
+
import { useOptionalCommunityContext } from "../../contexts/CommunityContext";
|
|
9
10
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* The membership invite token this visitor arrived holding, read from the
|
|
13
|
-
* `invite` search param of the CURRENT url (the invite email links to
|
|
14
|
-
* `/i/
|
|
14
|
+
* `invite` search param of the CURRENT url (the invite email links to the
|
|
15
|
+
* community's About page, `/i/communities/<slug>?invite=TOKEN`).
|
|
15
16
|
*
|
|
16
17
|
* Deliberately not stored anywhere: it is a bearer credential, and the durable
|
|
17
18
|
* copy is the email. Returns undefined on the server and when absent.
|
|
@@ -31,10 +32,17 @@ export interface UseGetMembershipTiersOptions {
|
|
|
31
32
|
* `null` forwards nothing even when the url carries one.
|
|
32
33
|
*/
|
|
33
34
|
inviteToken?: string | null;
|
|
35
|
+
/**
|
|
36
|
+
* The community whose tiers to list. Omitted, the surrounding
|
|
37
|
+
* `<CommunityProvider>` is used; outside one, every live tier on the profile
|
|
38
|
+
* comes back (a members-only gate on a product can name tiers from any
|
|
39
|
+
* community). Each tier carries its `communityId` either way.
|
|
40
|
+
*/
|
|
41
|
+
communityId?: string | null;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
/**
|
|
37
|
-
* `opts.enabled` gates the request on top of the usual profile/client checks
|
|
45
|
+
* `opts.enabled` gates the request on top of the usual profile/client checks:
|
|
38
46
|
* for a caller that only needs tier NAMES when something is actually gated, and
|
|
39
47
|
* must not add a request to every ungated storefront. Defaults to on, so every
|
|
40
48
|
* existing caller is unchanged.
|
|
@@ -44,22 +52,34 @@ export interface UseGetMembershipTiersOptions {
|
|
|
44
52
|
*/
|
|
45
53
|
export function useGetMembershipTiers(opts: UseGetMembershipTiersOptions = {}) {
|
|
46
54
|
const { client, profileId } = useForge();
|
|
55
|
+
const context = useOptionalCommunityContext();
|
|
47
56
|
const inviteToken = opts.inviteToken === undefined ? readMembershipInviteFromUrl() : (opts.inviteToken ?? undefined);
|
|
57
|
+
// `null` asks for the whole profile even inside a provider.
|
|
58
|
+
const communityId = opts.communityId === undefined ? context?.communityId : (opts.communityId ?? undefined);
|
|
59
|
+
// Inside a provider whose slug has not resolved yet, wait rather than list the whole profile.
|
|
60
|
+
const waitingForCommunity = opts.communityId === undefined && !!context && !context.communityId;
|
|
48
61
|
|
|
49
62
|
return useQuery<MembershipTier[]>({
|
|
50
|
-
// The token
|
|
51
|
-
// one every existing cache seed (SSR, specs)
|
|
52
|
-
|
|
63
|
+
// The community and the token are appended only when present, so the
|
|
64
|
+
// profile-wide key stays the one every existing cache seed (SSR, specs)
|
|
65
|
+
// already writes.
|
|
66
|
+
queryKey: [
|
|
67
|
+
"membership-tiers",
|
|
68
|
+
profileId,
|
|
69
|
+
...(communityId ? [{ communityId }] : []),
|
|
70
|
+
...(inviteToken ? [inviteToken] : []),
|
|
71
|
+
],
|
|
53
72
|
queryFn: async () => {
|
|
54
73
|
const res = await client.get("/public/membership-tiers", {
|
|
55
74
|
params: {
|
|
56
75
|
profileId: profileId,
|
|
76
|
+
...(communityId ? { communityId } : {}),
|
|
57
77
|
...(inviteToken ? { inviteToken } : {}),
|
|
58
78
|
},
|
|
59
79
|
});
|
|
60
80
|
return res.data;
|
|
61
81
|
},
|
|
62
|
-
enabled: !!profileId && !!client && opts.enabled !== false,
|
|
82
|
+
enabled: !!profileId && !!client && !waitingForCommunity && opts.enabled !== false,
|
|
63
83
|
});
|
|
64
84
|
}
|
|
65
85
|
|
|
@@ -109,6 +129,10 @@ export type ApplyForMembershipInput = {
|
|
|
109
129
|
questionnaire?: QuestionnaireAnswer[];
|
|
110
130
|
/** Where Stripe returns after a redirect-based card confirmation. */
|
|
111
131
|
returnUrl?: string;
|
|
132
|
+
/** A per-email invite token (`?invite=`). The community comes from the tier. */
|
|
133
|
+
inviteToken?: string;
|
|
134
|
+
/** A shareable invite link token (`?join=`). */
|
|
135
|
+
joinToken?: string;
|
|
112
136
|
};
|
|
113
137
|
|
|
114
138
|
/** Invalidate everything an application changes: the tier cards and the fan's list. */
|
|
@@ -118,6 +142,7 @@ function useInvalidateApplications() {
|
|
|
118
142
|
Promise.all([
|
|
119
143
|
queryClient.invalidateQueries({ queryKey: ["membership-tiers"] }),
|
|
120
144
|
queryClient.invalidateQueries({ queryKey: ["my-membership-applications"] }),
|
|
145
|
+
queryClient.invalidateQueries({ queryKey: ["public-communities"] }),
|
|
121
146
|
]);
|
|
122
147
|
}
|
|
123
148
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import type { AxiosInstance } from "axios";
|
|
2
3
|
import { useForge } from "../../provider/ForgeProvider";
|
|
4
|
+
import { MissingCommunityProviderError, useOptionalCommunityContext } from "../../contexts/CommunityContext";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* The viewer's own call calendar (`GET /public/scheduled-calls/mine`).
|
|
6
8
|
*
|
|
7
9
|
* Lives in Forge rather than in either app because BOTH rendering surfaces need
|
|
8
|
-
* exactly this -
|
|
9
|
-
*
|
|
10
|
+
* exactly this - every community's calendar tab and the classroom and cohort
|
|
11
|
+
* pages - and a
|
|
10
12
|
* second copy is how the two drift, as `useMyTickets` and `useMyBookings`
|
|
11
13
|
* already document.
|
|
12
14
|
*
|
|
@@ -37,6 +39,90 @@ import { useForge } from "../../provider/ForgeProvider";
|
|
|
37
39
|
|
|
38
40
|
export type MembershipCallParticipation = "open" | "host_only";
|
|
39
41
|
|
|
42
|
+
/** One speaker turn. Times are ms from the start of the recording, so they seek the video. */
|
|
43
|
+
export interface MemberCallTranscriptSegment {
|
|
44
|
+
name: string;
|
|
45
|
+
startMs: number;
|
|
46
|
+
endMs: number;
|
|
47
|
+
text: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A recorded call's transcript, as a member sees it: the finished thing, or
|
|
52
|
+
* that it is being prepared. Never a failure: the member is shown nothing at
|
|
53
|
+
* all rather than a reason that is the business's to deal with. Null on the
|
|
54
|
+
* recording when there is nothing to show.
|
|
55
|
+
*/
|
|
56
|
+
export type MemberCallTranscript =
|
|
57
|
+
| { status: "processing" }
|
|
58
|
+
| {
|
|
59
|
+
status: "ready";
|
|
60
|
+
/** Null when the summary could not be written; the transcript still stands. */
|
|
61
|
+
summary: MemberCallTranscriptSummary | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export interface MemberCallTranscriptSummary {
|
|
65
|
+
title: string;
|
|
66
|
+
summary: string;
|
|
67
|
+
decisions: string[];
|
|
68
|
+
actionItems: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One finished transcript with every line. NOT on the calendar payload: a
|
|
73
|
+
* two-hour meeting is thousands of lines, so they are read one transcript at a
|
|
74
|
+
* time, when a member opens it or downloads it.
|
|
75
|
+
*/
|
|
76
|
+
export interface MemberCallTranscriptDetail {
|
|
77
|
+
recordingId: string;
|
|
78
|
+
callId: string;
|
|
79
|
+
title: string;
|
|
80
|
+
startsAt: string;
|
|
81
|
+
timezone: string | null;
|
|
82
|
+
durationSeconds: number | null;
|
|
83
|
+
/** Signed for SIXTY seconds. Refetch for a fresh one when playback fails. */
|
|
84
|
+
watchUrl: string | null;
|
|
85
|
+
transcript: {
|
|
86
|
+
status: "ready";
|
|
87
|
+
language: string | null;
|
|
88
|
+
segments: MemberCallTranscriptSegment[];
|
|
89
|
+
summary: MemberCallTranscriptSummary | null;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const memberCallTranscriptKey = (profileId: string | undefined, recordingId: string) =>
|
|
94
|
+
["membership-calls", "transcript", profileId, recordingId] as const;
|
|
95
|
+
|
|
96
|
+
/** Read one transcript. Shared by the viewer and by Download, so both hit one cache entry. */
|
|
97
|
+
export const fetchMemberCallTranscript = async (
|
|
98
|
+
client: AxiosInstance,
|
|
99
|
+
profileId: string | undefined,
|
|
100
|
+
recordingId: string,
|
|
101
|
+
): Promise<MemberCallTranscriptDetail> => {
|
|
102
|
+
const res = await client.get(`/public/scheduled-calls/mine/recordings/${recordingId}/transcript`, {
|
|
103
|
+
params: { profileId },
|
|
104
|
+
});
|
|
105
|
+
return res.data as MemberCallTranscriptDetail;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* One recording's transcript and a fresh video address, for the viewer.
|
|
110
|
+
*
|
|
111
|
+
* Never refetched in the background: the lines do not change, and a refetch
|
|
112
|
+
* would swap the video address under a playing player. The viewer refetches on
|
|
113
|
+
* purpose when the sixty-second address lapses.
|
|
114
|
+
*/
|
|
115
|
+
export function useMemberCallTranscript(recordingId: string | null) {
|
|
116
|
+
const { client, profileId } = useForge();
|
|
117
|
+
return useQuery<MemberCallTranscriptDetail>({
|
|
118
|
+
queryKey: memberCallTranscriptKey(profileId, recordingId ?? ""),
|
|
119
|
+
queryFn: () => fetchMemberCallTranscript(client, profileId, recordingId!),
|
|
120
|
+
enabled: !!profileId && !!client && !!recordingId,
|
|
121
|
+
staleTime: Infinity,
|
|
122
|
+
refetchOnWindowFocus: false,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
40
126
|
export interface MyMembershipCall {
|
|
41
127
|
id: string;
|
|
42
128
|
title: string;
|
|
@@ -84,6 +170,8 @@ export interface MyMembershipCall {
|
|
|
84
170
|
durationSeconds: number | null;
|
|
85
171
|
/** Signed per request and short-lived. Null while `processing`. */
|
|
86
172
|
watchUrl: string | null;
|
|
173
|
+
/** Who said what, and a summary. Same access rule as the video. */
|
|
174
|
+
transcript: MemberCallTranscript | null;
|
|
87
175
|
} | null;
|
|
88
176
|
}
|
|
89
177
|
|
|
@@ -91,6 +179,21 @@ export const membershipCallKeys = {
|
|
|
91
179
|
mine: (profileId: string | undefined) => ["membership-calls", "mine", profileId] as const,
|
|
92
180
|
};
|
|
93
181
|
|
|
182
|
+
/**
|
|
183
|
+
* The community a call read is scoped to, or null for a course or cohort scope.
|
|
184
|
+
*
|
|
185
|
+
* A community calendar must come from `<CommunityProvider>`: a fan in two
|
|
186
|
+
* communities must never see one's calls on the other's calendar. A course or
|
|
187
|
+
* cohort scope names its own audience and belongs to no community, so it needs
|
|
188
|
+
* no provider. Anything else is a developer error, and it throws.
|
|
189
|
+
*/
|
|
190
|
+
function useCallScopeCommunity(hookName: string, courseScoped: boolean): string | null | undefined {
|
|
191
|
+
const community = useOptionalCommunityContext();
|
|
192
|
+
if (courseScoped) return null;
|
|
193
|
+
if (!community) throw new MissingCommunityProviderError(hookName);
|
|
194
|
+
return community.communityId;
|
|
195
|
+
}
|
|
196
|
+
|
|
94
197
|
/**
|
|
95
198
|
* @param options.from Inclusive lower bound. Absent means "from now on", which is
|
|
96
199
|
* what a caller wanting only the next few calls sends.
|
|
@@ -121,6 +224,8 @@ export function useMyMembershipCalls(options?: {
|
|
|
121
224
|
const to = options?.to?.toISOString();
|
|
122
225
|
const courseId = options?.courseId || undefined;
|
|
123
226
|
const cohortId = options?.cohortId || undefined;
|
|
227
|
+
// null = a course or cohort scope; a string = this community; undefined = still resolving.
|
|
228
|
+
const communityId = useCallScopeCommunity("useMyMembershipCalls()", !!(courseId || cohortId));
|
|
124
229
|
return useQuery<MyMembershipCall[]>({
|
|
125
230
|
// The WINDOW is part of the key: paging back a month is a different question
|
|
126
231
|
// with a different answer, and sharing one cache entry would show February's
|
|
@@ -134,14 +239,15 @@ export function useMyMembershipCalls(options?: {
|
|
|
134
239
|
to ?? "open",
|
|
135
240
|
courseId ?? "any-course",
|
|
136
241
|
cohortId ?? "any-cohort",
|
|
242
|
+
communityId ?? "no-community",
|
|
137
243
|
],
|
|
138
244
|
queryFn: async () => {
|
|
139
245
|
const res = await client.get("/public/scheduled-calls/mine", {
|
|
140
|
-
params: { profileId, from, to, limit: options?.limit, courseId, cohortId },
|
|
246
|
+
params: { profileId, from, to, limit: options?.limit, courseId, cohortId, communityId: communityId ?? undefined },
|
|
141
247
|
});
|
|
142
248
|
return res.data;
|
|
143
249
|
},
|
|
144
|
-
enabled: !!profileId && !!client && options?.enabled !== false,
|
|
250
|
+
enabled: !!profileId && !!client && communityId !== undefined && options?.enabled !== false,
|
|
145
251
|
/**
|
|
146
252
|
* A call opens fifteen minutes before it starts, and the member watching
|
|
147
253
|
* this page is waiting for exactly that. Without a refetch the Join button
|
|
@@ -175,19 +281,22 @@ export interface MyMembershipCallRecording {
|
|
|
175
281
|
* for long. Null while `status` is `processing`.
|
|
176
282
|
*/
|
|
177
283
|
watchUrl: string | null;
|
|
284
|
+
/** Who said what, and a summary. Same access rule as the video. */
|
|
285
|
+
transcript: MemberCallTranscript | null;
|
|
178
286
|
}
|
|
179
287
|
|
|
180
288
|
export function useMyMembershipCallRecordings(options?: { limit?: number; enabled?: boolean }) {
|
|
181
289
|
const { client, profileId } = useForge();
|
|
290
|
+
const communityId = useCallScopeCommunity("useMyMembershipCallRecordings()", false);
|
|
182
291
|
return useQuery<MyMembershipCallRecording[]>({
|
|
183
|
-
queryKey: ["membership-calls", "recordings", profileId],
|
|
292
|
+
queryKey: ["membership-calls", "recordings", profileId, communityId],
|
|
184
293
|
queryFn: async () => {
|
|
185
294
|
const res = await client.get("/public/scheduled-calls/mine/recordings", {
|
|
186
|
-
params: { profileId, limit: options?.limit },
|
|
295
|
+
params: { profileId, communityId, limit: options?.limit },
|
|
187
296
|
});
|
|
188
297
|
return res.data;
|
|
189
298
|
},
|
|
190
|
-
enabled: !!profileId && !!client && options?.enabled !== false,
|
|
299
|
+
enabled: !!profileId && !!communityId && !!client && options?.enabled !== false,
|
|
191
300
|
/**
|
|
192
301
|
* Slower than the upcoming list: nothing here changes on a fifteen-minute
|
|
193
302
|
* boundary, and the only transition worth catching is `processing` ->
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
1
|
+
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
2
|
import { useForge } from "../../provider/ForgeProvider";
|
|
3
3
|
import type { PaginatedData } from "../../types/models";
|
|
4
4
|
|
|
@@ -279,3 +279,26 @@ export function useCancelMyBooking() {
|
|
|
279
279
|
},
|
|
280
280
|
});
|
|
281
281
|
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Every session, a page at a time, for a list with a "Show more" button.
|
|
285
|
+
*
|
|
286
|
+
* Under the same `my-bookings` key prefix as `useMyBookings`, so cancelling or
|
|
287
|
+
* rescheduling refreshes this too.
|
|
288
|
+
*/
|
|
289
|
+
export function useMyBookingsPaged(accountId?: string, limit = 20) {
|
|
290
|
+
const { client, profileId } = useForge();
|
|
291
|
+
return useInfiniteQuery({
|
|
292
|
+
queryKey: ["my-bookings", accountId, profileId, "paged", limit],
|
|
293
|
+
initialPageParam: 1,
|
|
294
|
+
queryFn: async ({ pageParam }) => {
|
|
295
|
+
const res = await client.get("/public/coaching/bookings/mine", { params: { profileId, page: pageParam, limit } });
|
|
296
|
+
return res.data as PaginatedData<MyBooking>;
|
|
297
|
+
},
|
|
298
|
+
getNextPageParam: (last, pages) => {
|
|
299
|
+
const loaded = pages.reduce((sum, p) => sum + p.data.length, 0);
|
|
300
|
+
return loaded < last.total && last.data.length > 0 ? pages.length + 1 : undefined;
|
|
301
|
+
},
|
|
302
|
+
enabled: !!accountId && !!profileId && !!client,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useForge } from "../../provider/ForgeProvider";
|
|
2
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
3
|
import type { PaginatedData } from "../../types/models";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -313,3 +313,26 @@ export function useCancelMyTicket() {
|
|
|
313
313
|
},
|
|
314
314
|
});
|
|
315
315
|
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Every ticket, a page at a time, for a list with a "Show more" button.
|
|
319
|
+
*
|
|
320
|
+
* Under the same `my-tickets` key prefix as `useMyTickets`, so the cancel and
|
|
321
|
+
* transfer mutations that invalidate that prefix refresh this too.
|
|
322
|
+
*/
|
|
323
|
+
export function useMyTicketsPaged(accountId?: string, limit = 20) {
|
|
324
|
+
const { client, profileId } = useForge();
|
|
325
|
+
return useInfiniteQuery({
|
|
326
|
+
queryKey: ["my-tickets", accountId, profileId, "paged", limit],
|
|
327
|
+
initialPageParam: 1,
|
|
328
|
+
queryFn: async ({ pageParam }) => {
|
|
329
|
+
const res = await client.get("/public/events/my-tickets", { params: { profileId, page: pageParam, limit } });
|
|
330
|
+
return res.data as PaginatedData<MyTicket>;
|
|
331
|
+
},
|
|
332
|
+
getNextPageParam: (last, pages) => {
|
|
333
|
+
const loaded = pages.reduce((sum, p) => sum + p.data.length, 0);
|
|
334
|
+
return loaded < last.total && last.data.length > 0 ? pages.length + 1 : undefined;
|
|
335
|
+
},
|
|
336
|
+
enabled: !!accountId && !!profileId && !!client,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Member notification headless hooks
|
|
6
|
+
* Member notification headless hooks: the FROZEN public contract that the
|
|
7
7
|
* apps/client PWA and every generated custom-code site consume. Speaks ONLY the
|
|
8
8
|
* `/public/notifications/*` API (see docs/archive/shipped/membership-notifications-initiative.md
|
|
9
9
|
* "API surface"). Moved here from apps/client so custom-code sites get push
|
|
@@ -56,6 +56,10 @@ export interface NotificationInboxItem {
|
|
|
56
56
|
url?: string;
|
|
57
57
|
actorName?: string;
|
|
58
58
|
actorAccountId?: string;
|
|
59
|
+
/** A `mod` notice about the member's own post or reply: what the moderator decided. */
|
|
60
|
+
outcome?: "approved" | "rejected";
|
|
61
|
+
/** The moderator's reason for a rejection, when one was given. */
|
|
62
|
+
reason?: string | null;
|
|
59
63
|
} | null;
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -218,7 +222,7 @@ export function useNotificationUnreadCount(enabled = true) {
|
|
|
218
222
|
});
|
|
219
223
|
}
|
|
220
224
|
|
|
221
|
-
/** Mark notifications read
|
|
225
|
+
/** Mark notifications read. Omit ids to mark ALL unread as read. */
|
|
222
226
|
export function useMarkNotificationsRead() {
|
|
223
227
|
const { client, profileId } = useForge();
|
|
224
228
|
const queryClient = useQueryClient();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useForge } from "../../provider/ForgeProvider";
|
|
2
|
+
import { readMetaSignals } from "../../utils/metaEvents";
|
|
2
3
|
import { useMutation } from "@tanstack/react-query";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -74,7 +75,7 @@ export function useCreateOrder() {
|
|
|
74
75
|
|
|
75
76
|
return useMutation<CreateOrderResult, unknown, CreateOrderInput>({
|
|
76
77
|
mutationFn: async (input) => {
|
|
77
|
-
const res = await client.post("/public/orders", { ...input, profileId });
|
|
78
|
+
const res = await client.post("/public/orders", { ...input, profileId, metaSignals: readMetaSignals() });
|
|
78
79
|
return res.data;
|
|
79
80
|
},
|
|
80
81
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { shareRuntimeValue } from "../../runtime/registry";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* Headless PWA install-prompt hook
|
|
5
|
+
* Headless PWA install-prompt hook: the versioned Forge primitive that powers
|
|
5
6
|
* `<InstallBanner/>` (forge/ui) and any custom install UI on a code site. Ported
|
|
6
7
|
* from apps/client/src/lib/hooks/usePWA.ts, plus iOS + standalone detection
|
|
7
8
|
* (Safari never fires `beforeinstallprompt`, so iOS needs the manual
|
|
@@ -22,7 +23,7 @@ export interface InstallPromptState {
|
|
|
22
23
|
isInstallable: boolean;
|
|
23
24
|
/** The app is running installed (standalone display-mode or iOS standalone). */
|
|
24
25
|
isInstalled: boolean;
|
|
25
|
-
/** iOS Safari
|
|
26
|
+
/** iOS Safari has no `beforeinstallprompt`: show the Add-to-Home-Screen guide. */
|
|
26
27
|
isIOS: boolean;
|
|
27
28
|
/** Currently rendered in standalone (installed) display mode. */
|
|
28
29
|
isStandalone: boolean;
|
|
@@ -47,8 +48,108 @@ function detectStandalone(): boolean {
|
|
|
47
48
|
return !!mm || iosStandalone;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
// ---- The captured install prompt, shared ----------------------------------
|
|
52
|
+
//
|
|
53
|
+
// The browser fires `beforeinstallprompt` ONCE per page load, usually soon after
|
|
54
|
+
// it. A listener added later (a prompt mounted after the fan joins a community)
|
|
55
|
+
// would never hear it, so the event is captured the first time anything asks,
|
|
56
|
+
// and every reader shares the one captured event.
|
|
57
|
+
//
|
|
58
|
+
// Shared through the runtime registry, not a module variable: a site and the
|
|
59
|
+
// hosted `/i/` pages bundle each carry a copy of Forge, and the site's copy
|
|
60
|
+
// (`<PwaRegistration>`, at the root) is the one that hears the event.
|
|
61
|
+
|
|
62
|
+
interface InstallPromptStore {
|
|
63
|
+
prompt: BeforeInstallPromptEvent | null;
|
|
64
|
+
installed: boolean;
|
|
65
|
+
capturing: boolean;
|
|
66
|
+
listeners: Set<() => void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const store = (): InstallPromptStore =>
|
|
70
|
+
shareRuntimeValue<InstallPromptStore>("state:pwa-install-prompt", () => ({
|
|
71
|
+
prompt: null,
|
|
72
|
+
installed: false,
|
|
73
|
+
capturing: false,
|
|
74
|
+
listeners: new Set(),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const notifyPrompt = () => store().listeners.forEach((listener) => listener());
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Start listening for `beforeinstallprompt` (and `appinstalled`). Idempotent and
|
|
81
|
+
* SSR-safe. `<PwaRegistration enabled>` calls it at mount so the event is held
|
|
82
|
+
* from the first page a visitor sees.
|
|
83
|
+
*/
|
|
84
|
+
export function captureInstallPrompt(): void {
|
|
85
|
+
const state = store();
|
|
86
|
+
if (state.capturing || typeof window === "undefined") return;
|
|
87
|
+
state.capturing = true;
|
|
88
|
+
window.addEventListener("beforeinstallprompt", (event: Event) => {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
store().prompt = event as BeforeInstallPromptEvent;
|
|
91
|
+
notifyPrompt();
|
|
92
|
+
});
|
|
93
|
+
window.addEventListener("appinstalled", () => {
|
|
94
|
+
store().prompt = null;
|
|
95
|
+
store().installed = true;
|
|
96
|
+
notifyPrompt();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** True when a native install prompt is held and can be shown. */
|
|
101
|
+
export function hasStoredInstallPrompt(): boolean {
|
|
102
|
+
return !!store().prompt;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** True once the app was installed during this page's life. */
|
|
106
|
+
export function wasInstalledThisSession(): boolean {
|
|
107
|
+
return store().installed;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Be told when the held prompt arrives, is used, or the app is installed. */
|
|
111
|
+
export function subscribeInstallPrompt(listener: () => void): () => void {
|
|
112
|
+
store().listeners.add(listener);
|
|
113
|
+
return () => {
|
|
114
|
+
store().listeners.delete(listener);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Show the held native prompt. Resolves true when the visitor accepted. A
|
|
120
|
+
* prompt can be shown once, so it is released either way.
|
|
121
|
+
*/
|
|
122
|
+
export async function promptStoredInstall(): Promise<boolean> {
|
|
123
|
+
const state = store();
|
|
124
|
+
const event = state.prompt;
|
|
125
|
+
if (!event) return false;
|
|
126
|
+
state.prompt = null;
|
|
127
|
+
notifyPrompt();
|
|
128
|
+
try {
|
|
129
|
+
await event.prompt();
|
|
130
|
+
const { outcome } = await event.userChoice;
|
|
131
|
+
if (outcome === "accepted") {
|
|
132
|
+
state.installed = true;
|
|
133
|
+
notifyPrompt();
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Test seam: forget whatever was captured, or hold `event` as if the browser had fired it. */
|
|
143
|
+
export function resetInstallPromptForTests(event: Event | null = null): void {
|
|
144
|
+
const state = store();
|
|
145
|
+
state.prompt = event as BeforeInstallPromptEvent | null;
|
|
146
|
+
state.installed = false;
|
|
147
|
+
notifyPrompt();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { detectIOS as isIOSDevice, detectStandalone as isStandaloneDisplay };
|
|
151
|
+
|
|
50
152
|
export function useInstallPrompt(): InstallPromptState {
|
|
51
|
-
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
|
52
153
|
const [isInstallable, setIsInstallable] = useState(false);
|
|
53
154
|
const [isInstalled, setIsInstalled] = useState(false);
|
|
54
155
|
const [isIOS, setIsIOS] = useState(false);
|
|
@@ -61,41 +162,16 @@ export function useInstallPrompt(): InstallPromptState {
|
|
|
61
162
|
setIsStandalone(standalone);
|
|
62
163
|
if (standalone) setIsInstalled(true);
|
|
63
164
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
};
|
|
69
|
-
const handleAppInstalled = () => {
|
|
70
|
-
setIsInstalled(true);
|
|
71
|
-
setIsInstallable(false);
|
|
72
|
-
setDeferredPrompt(null);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
|
76
|
-
window.addEventListener("appinstalled", handleAppInstalled);
|
|
77
|
-
return () => {
|
|
78
|
-
window.removeEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
|
79
|
-
window.removeEventListener("appinstalled", handleAppInstalled);
|
|
165
|
+
captureInstallPrompt();
|
|
166
|
+
const sync = () => {
|
|
167
|
+
setIsInstallable(hasStoredInstallPrompt());
|
|
168
|
+
if (wasInstalledThisSession()) setIsInstalled(true);
|
|
80
169
|
};
|
|
170
|
+
sync();
|
|
171
|
+
return subscribeInstallPrompt(sync);
|
|
81
172
|
}, []);
|
|
82
173
|
|
|
83
|
-
const promptInstall = useCallback(
|
|
84
|
-
if (!deferredPrompt) return false;
|
|
85
|
-
try {
|
|
86
|
-
await deferredPrompt.prompt();
|
|
87
|
-
const { outcome } = await deferredPrompt.userChoice;
|
|
88
|
-
if (outcome === "accepted") {
|
|
89
|
-
setIsInstalled(true);
|
|
90
|
-
setIsInstallable(false);
|
|
91
|
-
setDeferredPrompt(null);
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
95
|
-
} catch {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
}, [deferredPrompt]);
|
|
174
|
+
const promptInstall = useCallback(() => promptStoredInstall(), []);
|
|
99
175
|
|
|
100
176
|
return { isInstallable, isInstalled, isIOS, isStandalone, promptInstall };
|
|
101
177
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IPublicPostCollection, PaginatedData } from "../../types/models";
|
|
2
|
-
import {
|
|
2
|
+
import { useCommunityScope } from "../../contexts/CommunityContext";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
|
|
5
5
|
export interface GetPostCollectionsParams {
|
|
@@ -8,14 +8,15 @@ export interface GetPostCollectionsParams {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function useGetPostCollections(params?: GetPostCollectionsParams) {
|
|
11
|
-
const { client, profileId } =
|
|
11
|
+
const { client, profileId, communityId } = useCommunityScope("useGetPostCollections()");
|
|
12
12
|
|
|
13
13
|
return useQuery<PaginatedData<IPublicPostCollection>>({
|
|
14
|
-
queryKey: ["post-collections",
|
|
14
|
+
queryKey: ["post-collections", communityId, params],
|
|
15
15
|
queryFn: async () => {
|
|
16
16
|
const res = await client.get("/public/post-collections", {
|
|
17
17
|
params: {
|
|
18
|
-
profileId
|
|
18
|
+
profileId,
|
|
19
|
+
communityId,
|
|
19
20
|
page: params?.page || 1,
|
|
20
21
|
limit: 10,
|
|
21
22
|
filter: {
|
|
@@ -25,24 +26,35 @@ export function useGetPostCollections(params?: GetPostCollectionsParams) {
|
|
|
25
26
|
});
|
|
26
27
|
return res.data;
|
|
27
28
|
},
|
|
28
|
-
enabled: !!profileId && !!client,
|
|
29
|
+
enabled: !!profileId && !!communityId && !!client,
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* One collection, by id or slug. A collection that is missing, archived or in
|
|
35
|
+
* another community answers 404, which is an ANSWER, not a failure: this
|
|
36
|
+
* resolves `null` for it (and does not retry), so a page can say "not found"
|
|
37
|
+
* rather than falling over.
|
|
38
|
+
*/
|
|
32
39
|
export function useGetPostCollection(collectionId?: string) {
|
|
33
|
-
const { client, profileId } =
|
|
40
|
+
const { client, profileId, communityId } = useCommunityScope("useGetPostCollection()");
|
|
34
41
|
|
|
35
|
-
return useQuery<IPublicPostCollection>({
|
|
36
|
-
queryKey: ["post-collection",
|
|
42
|
+
return useQuery<IPublicPostCollection | null>({
|
|
43
|
+
queryKey: ["post-collection", communityId, collectionId],
|
|
37
44
|
queryFn: async () => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
try {
|
|
46
|
+
const res = await client.get(`/public/post-collections/${collectionId}`, {
|
|
47
|
+
params: {
|
|
48
|
+
profileId,
|
|
49
|
+
communityId,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
return res.data ?? null;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if ((error as { response?: { status?: number } })?.response?.status === 404) return null;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
44
57
|
},
|
|
45
|
-
enabled: !!profileId && !!client && !!collectionId,
|
|
58
|
+
enabled: !!profileId && !!communityId && !!client && !!collectionId,
|
|
46
59
|
});
|
|
47
60
|
}
|
|
48
|
-
|