@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
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
keepPreviousData,
|
|
4
|
+
useInfiniteQuery,
|
|
5
|
+
useMutation,
|
|
6
|
+
useQuery,
|
|
7
|
+
useQueryClient,
|
|
8
|
+
type QueryClient,
|
|
9
|
+
} from "@tanstack/react-query";
|
|
10
|
+
import { useCommunityScope } from "../../contexts/CommunityContext";
|
|
3
11
|
import { useChatSocket } from "./useChat";
|
|
12
|
+
import type { CommunityAgentAuthor } from "./useCommunityAgent";
|
|
4
13
|
|
|
5
14
|
// ── Types (mirror apps/backend/src/routes/public/community + models) ─────────
|
|
6
15
|
|
|
@@ -17,7 +26,7 @@ export interface CommunitySpace {
|
|
|
17
26
|
order: number;
|
|
18
27
|
archivedAt: string | null;
|
|
19
28
|
createdAt: string;
|
|
20
|
-
/** Gating tiers
|
|
29
|
+
/** Gating tiers: empty array = open to every member. */
|
|
21
30
|
tiers: { id: string; name: string }[];
|
|
22
31
|
/** Only present on the member list query. */
|
|
23
32
|
postCount?: number | null;
|
|
@@ -25,6 +34,14 @@ export interface CommunitySpace {
|
|
|
25
34
|
memberCount?: number | null;
|
|
26
35
|
/** Whether the current member has joined this space (new-post notifications). */
|
|
27
36
|
isJoined?: boolean;
|
|
37
|
+
/** The level a member needs to read this space (1 = no level needed). */
|
|
38
|
+
minLevel?: number;
|
|
39
|
+
/**
|
|
40
|
+
* On the member list: the space is above the member's level. It is listed so
|
|
41
|
+
* they can see what their next level opens, but its feed stays closed (the
|
|
42
|
+
* server refuses it too).
|
|
43
|
+
*/
|
|
44
|
+
levelLocked?: boolean;
|
|
28
45
|
}
|
|
29
46
|
|
|
30
47
|
/** One roster row (GET /public/community/spaces/:id/members). */
|
|
@@ -80,7 +97,8 @@ export interface CommunityPost {
|
|
|
80
97
|
id: string;
|
|
81
98
|
profileId: string;
|
|
82
99
|
spaceId: string;
|
|
83
|
-
|
|
100
|
+
/** Null when the community AI wrote it (`agentAuthor` is then set). */
|
|
101
|
+
authorAccountId: string | null;
|
|
84
102
|
title: string | null;
|
|
85
103
|
/** Lexical editor state (JSON). Render with <CommunityRichText/>. */
|
|
86
104
|
body: unknown;
|
|
@@ -92,22 +110,119 @@ export interface CommunityPost {
|
|
|
92
110
|
lastActivityAt: string | null;
|
|
93
111
|
createdAt: string;
|
|
94
112
|
author: CommunityAuthor | null;
|
|
113
|
+
/** The community AI, when it wrote this post. `author` is then null. Absent on older servers. */
|
|
114
|
+
agentAuthor?: CommunityAgentAuthor | null;
|
|
95
115
|
media: CommunityMediaItem[];
|
|
96
116
|
reactions?: ReactionSummary[];
|
|
117
|
+
/**
|
|
118
|
+
* Moderation state. Only an author sees their own pending post; everyone else
|
|
119
|
+
* sees published posts only. Absent on rows from older servers: treat as published.
|
|
120
|
+
*/
|
|
121
|
+
status?: CommunityContentStatus;
|
|
122
|
+
/** Why a post was held (`approval_new_member`, `approval_all`, `keyword:<word>`), or why it was rejected. */
|
|
123
|
+
moderationReason?: string | null;
|
|
124
|
+
/** Set in the cache when a moderator removes the post in this session, so the undo stays in place. */
|
|
125
|
+
archivedAt?: string | null;
|
|
126
|
+
/**
|
|
127
|
+
* The post's poll, as this viewer sees it, or null when it has none. Absent
|
|
128
|
+
* on a post that arrived over the realtime socket (see `CommunityPollView`).
|
|
129
|
+
*/
|
|
130
|
+
poll?: CommunityPollView | null;
|
|
131
|
+
/**
|
|
132
|
+
* A card for the first link in the post, or null. Filled in the background
|
|
133
|
+
* shortly after the post is published, so a new post has none at first.
|
|
134
|
+
* Absent on rows from older servers.
|
|
135
|
+
*/
|
|
136
|
+
linkPreview?: CommunityLinkPreview | null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** A link card on a post (community parity §6.2). */
|
|
140
|
+
export interface CommunityLinkPreview {
|
|
141
|
+
url: string;
|
|
142
|
+
kind: "link" | "video";
|
|
143
|
+
provider: "youtube" | "vimeo" | "loom" | null;
|
|
144
|
+
siteName: string | null;
|
|
145
|
+
title: string | null;
|
|
146
|
+
description: string | null;
|
|
147
|
+
imageUrl: string | null;
|
|
148
|
+
/** The player address, for a video. Render only through `safeEmbedUrl`. */
|
|
149
|
+
embedUrl: string | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Polls (community parity §6.1) ────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/** When a poll's counts are shown: at once, once the member voted, or once it closed. */
|
|
155
|
+
export type CommunityPollResultsVisibility = "always" | "after_vote" | "after_close";
|
|
156
|
+
|
|
157
|
+
export const COMMUNITY_POLL_RESULTS_VISIBILITY: readonly CommunityPollResultsVisibility[] = [
|
|
158
|
+
"always",
|
|
159
|
+
"after_vote",
|
|
160
|
+
"after_close",
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
export interface CommunityPollOption {
|
|
164
|
+
id: string;
|
|
165
|
+
label: string;
|
|
166
|
+
position: number;
|
|
167
|
+
/** Null while this viewer may not see results yet. */
|
|
168
|
+
votes: number | null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A post's poll as the viewer sees it. The post title is its question. */
|
|
172
|
+
export interface CommunityPollView {
|
|
173
|
+
id: string;
|
|
174
|
+
multipleChoice: boolean;
|
|
175
|
+
closesAt: string | null;
|
|
176
|
+
closed: boolean;
|
|
177
|
+
resultsVisibility: CommunityPollResultsVisibility;
|
|
178
|
+
/** Whether this viewer may see the counts now. */
|
|
179
|
+
resultsVisible: boolean;
|
|
180
|
+
/** Distinct members who voted. Null while results are hidden. */
|
|
181
|
+
totalVoters: number | null;
|
|
182
|
+
/** The option ids this viewer chose. Empty when they have not voted. */
|
|
183
|
+
myVotes: string[];
|
|
184
|
+
options: CommunityPollOption[];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The poll part of the create-post body. The question travels as the post `title`. */
|
|
188
|
+
export interface CommunityPollInput {
|
|
189
|
+
options: string[];
|
|
190
|
+
multipleChoice?: boolean;
|
|
191
|
+
/** ISO date, in the future. */
|
|
192
|
+
closesAt?: string | null;
|
|
193
|
+
resultsVisibility?: CommunityPollResultsVisibility;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Where a post or reply stands in moderation. */
|
|
197
|
+
export type CommunityContentStatus = "published" | "pending" | "rejected";
|
|
198
|
+
|
|
199
|
+
/** True when a post or reply is waiting for a moderator (only its author ever sees it). */
|
|
200
|
+
export function isCommunityContentPending(
|
|
201
|
+
item: { status?: CommunityContentStatus | string | null } | null | undefined,
|
|
202
|
+
): boolean {
|
|
203
|
+
return item?.status === "pending";
|
|
97
204
|
}
|
|
98
205
|
|
|
99
206
|
export interface CommunityReply {
|
|
100
207
|
id: string;
|
|
101
208
|
postId: string;
|
|
102
209
|
parentReplyId: string | null;
|
|
103
|
-
|
|
210
|
+
/** Null when the community AI wrote it (`agentAuthor` is then set). */
|
|
211
|
+
authorAccountId: string | null;
|
|
104
212
|
body: unknown;
|
|
105
213
|
bodyText: string | null;
|
|
106
214
|
reactionCount: number;
|
|
107
215
|
createdAt: string;
|
|
108
216
|
author: CommunityAuthor | null;
|
|
217
|
+
/** The community AI, when this reply is its answer. `author` is then null. */
|
|
218
|
+
agentAuthor?: CommunityAgentAuthor | null;
|
|
109
219
|
media: CommunityMediaItem[];
|
|
110
220
|
reactions?: ReactionSummary[];
|
|
221
|
+
/** See `CommunityPost.status`. */
|
|
222
|
+
status?: CommunityContentStatus;
|
|
223
|
+
moderationReason?: string | null;
|
|
224
|
+
/** Set in the cache when a moderator removes the reply in this session. */
|
|
225
|
+
archivedAt?: string | null;
|
|
111
226
|
}
|
|
112
227
|
|
|
113
228
|
/** Attachment input shape the community create endpoints accept. */
|
|
@@ -136,6 +251,20 @@ export interface CommunityMemberProfile {
|
|
|
136
251
|
createdAt: string;
|
|
137
252
|
firstName: string | null;
|
|
138
253
|
lastName: string | null;
|
|
254
|
+
/**
|
|
255
|
+
* Only on `/me`: whether this member moderates THIS community (the moderator
|
|
256
|
+
* role here, or an owner/admin of the profile). Display only; the server
|
|
257
|
+
* checks again on every moderation call.
|
|
258
|
+
*/
|
|
259
|
+
canModerate?: boolean;
|
|
260
|
+
/** Listed in the member directory (default true). */
|
|
261
|
+
showInDirectory?: boolean;
|
|
262
|
+
/** Counted on the member map, by country only (default false). */
|
|
263
|
+
showOnMap?: boolean;
|
|
264
|
+
/** Free text, display only. */
|
|
265
|
+
city?: string | null;
|
|
266
|
+
/** ISO 3166-1 alpha-2, or null. */
|
|
267
|
+
countryCode?: string | null;
|
|
139
268
|
recentPosts?: {
|
|
140
269
|
id: string;
|
|
141
270
|
spaceId: string;
|
|
@@ -158,17 +287,30 @@ export interface CommunityMemberProfile {
|
|
|
158
287
|
|
|
159
288
|
export type CommunityDigestCadence = "off" | "inapp_only" | "hourly" | "daily";
|
|
160
289
|
|
|
161
|
-
export type CommunityNotificationType = "mention" | "reply" | "reaction" | "new_post" | "mod";
|
|
290
|
+
export type CommunityNotificationType = "mention" | "reply" | "reaction" | "new_post" | "mod" | "level_up";
|
|
162
291
|
|
|
163
292
|
export interface CommunityNotificationItem {
|
|
164
293
|
id: string;
|
|
165
294
|
profileId: string;
|
|
166
295
|
accountId: string;
|
|
167
296
|
type: CommunityNotificationType | string;
|
|
168
|
-
entityType: "community_post" | "community_reply" | string | null;
|
|
297
|
+
entityType: "community_post" | "community_reply" | "community" | string | null;
|
|
169
298
|
entityId: string | null;
|
|
170
299
|
readAt: string | null;
|
|
171
300
|
createdAt: string;
|
|
301
|
+
/**
|
|
302
|
+
* Extra detail for some types. A `mod` notice about a post or reply carries
|
|
303
|
+
* the moderation outcome; a `mod` notice about the community (a mute) has none.
|
|
304
|
+
*/
|
|
305
|
+
data?: CommunityNotificationData | null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export interface CommunityNotificationData {
|
|
309
|
+
outcome?: "approved" | "rejected";
|
|
310
|
+
reason?: string | null;
|
|
311
|
+
/** On a `level_up`: the level the member reached. */
|
|
312
|
+
level?: number;
|
|
313
|
+
[key: string]: unknown;
|
|
172
314
|
}
|
|
173
315
|
|
|
174
316
|
export interface CommunityLeaderboardEntry {
|
|
@@ -234,35 +376,40 @@ export function communityMemberName(
|
|
|
234
376
|
}
|
|
235
377
|
|
|
236
378
|
// Query keys are exported so the realtime layer (and creators) can patch the
|
|
237
|
-
// same caches the data hooks populate
|
|
379
|
+
// same caches the data hooks populate: mirrors chatKeys in useChat.ts.
|
|
238
380
|
export const communityKeys = {
|
|
239
|
-
|
|
381
|
+
// Scoped by COMMUNITY, not profile: a profile runs several, and one cache
|
|
382
|
+
// entry per profile would show community A's spaces inside community B.
|
|
383
|
+
spaces: (communityId?: string) => ["public-community-spaces", communityId] as const,
|
|
240
384
|
spaceMembers: (spaceId?: string) => ["public-community-space-members", spaceId] as const,
|
|
241
385
|
posts: (spaceId?: string) => ["public-community-posts", spaceId] as const,
|
|
242
386
|
post: (postId?: string) => ["public-community-post", postId] as const,
|
|
243
387
|
replies: (postId?: string) => ["public-community-replies", postId] as const,
|
|
244
|
-
myProfile: (
|
|
245
|
-
member: (
|
|
246
|
-
notifications: (
|
|
247
|
-
["public-community-notifications",
|
|
248
|
-
unread: (
|
|
249
|
-
leaderboard: (
|
|
250
|
-
search: (
|
|
251
|
-
["public-community-search",
|
|
388
|
+
myProfile: (communityId?: string) => ["public-community-my-profile", communityId] as const,
|
|
389
|
+
member: (communityId?: string, accountId?: string) => ["public-community-member", communityId, accountId] as const,
|
|
390
|
+
notifications: (communityId?: string, unreadOnly?: boolean) =>
|
|
391
|
+
["public-community-notifications", communityId, unreadOnly] as const,
|
|
392
|
+
unread: (communityId?: string) => ["public-community-unread", communityId] as const,
|
|
393
|
+
leaderboard: (communityId?: string, period?: string) => ["public-community-leaderboard", communityId, period] as const,
|
|
394
|
+
search: (communityId?: string, q?: string, spaceId?: string) =>
|
|
395
|
+
["public-community-search", communityId, q, spaceId ?? null] as const,
|
|
396
|
+
moderationQueue: (communityId?: string) => ["public-community-moderation-queue", communityId] as const,
|
|
397
|
+
moderationLog: (communityId?: string, page?: number) =>
|
|
398
|
+
["public-community-moderation-log", communityId, page] as const,
|
|
252
399
|
};
|
|
253
400
|
|
|
254
401
|
// ── Data hooks ───────────────────────────────────────────────────────────────
|
|
255
402
|
|
|
256
403
|
/** Spaces the current member can access (GET /public/community/spaces). */
|
|
257
404
|
export function useCommunitySpaces() {
|
|
258
|
-
const { client, profileId } =
|
|
405
|
+
const { client, profileId, communityId } = useCommunityScope("useCommunitySpaces()");
|
|
259
406
|
return useQuery<CommunitySpace[]>({
|
|
260
|
-
queryKey: communityKeys.spaces(
|
|
407
|
+
queryKey: communityKeys.spaces(communityId),
|
|
261
408
|
queryFn: async () => {
|
|
262
|
-
const res = await client.get("/public/community/spaces", { params: { profileId } });
|
|
409
|
+
const res = await client.get("/public/community/spaces", { params: { profileId, communityId } });
|
|
263
410
|
return res.data;
|
|
264
411
|
},
|
|
265
|
-
enabled: !!profileId && !!client,
|
|
412
|
+
enabled: !!profileId && !!communityId && !!client,
|
|
266
413
|
});
|
|
267
414
|
}
|
|
268
415
|
|
|
@@ -274,30 +421,30 @@ export interface SpaceMembershipResult {
|
|
|
274
421
|
/** Patch one space's isJoined/memberCount inside the spaces list cache. */
|
|
275
422
|
export function setSpaceMembership(
|
|
276
423
|
queryClient: QueryClient,
|
|
277
|
-
|
|
424
|
+
communityId: string | undefined,
|
|
278
425
|
spaceId: string,
|
|
279
426
|
membership: SpaceMembershipResult,
|
|
280
427
|
) {
|
|
281
|
-
queryClient.setQueryData(communityKeys.spaces(
|
|
428
|
+
queryClient.setQueryData(communityKeys.spaces(communityId), (old: CommunitySpace[] | undefined) =>
|
|
282
429
|
old?.map((s) => (s.id === spaceId ? { ...s, ...membership } : s)),
|
|
283
430
|
);
|
|
284
431
|
}
|
|
285
432
|
|
|
286
433
|
/**
|
|
287
434
|
* Join a space (POST /public/community/spaces/:id/join). Tiers still govern
|
|
288
|
-
* who can access a space
|
|
435
|
+
* who can access a space: joining opts into new-post notifications/digests
|
|
289
436
|
* and the roster. Interacting (post/reply/react) auto-joins server-side.
|
|
290
437
|
*/
|
|
291
438
|
export function useJoinCommunitySpace() {
|
|
292
|
-
const { client, profileId } =
|
|
439
|
+
const { client, profileId, communityId } = useCommunityScope("useJoinCommunitySpace()");
|
|
293
440
|
const queryClient = useQueryClient();
|
|
294
441
|
return useMutation({
|
|
295
442
|
mutationFn: async (spaceId: string) => {
|
|
296
|
-
const res = await client.post(`/public/community/spaces/${spaceId}/join`, { profileId });
|
|
443
|
+
const res = await client.post(`/public/community/spaces/${spaceId}/join`, { profileId, communityId });
|
|
297
444
|
return { ...(res.data as SpaceMembershipResult), spaceId };
|
|
298
445
|
},
|
|
299
446
|
onSuccess: ({ spaceId, ...membership }) => {
|
|
300
|
-
setSpaceMembership(queryClient,
|
|
447
|
+
setSpaceMembership(queryClient, communityId, spaceId, membership);
|
|
301
448
|
queryClient.invalidateQueries({ queryKey: communityKeys.spaceMembers(spaceId) });
|
|
302
449
|
},
|
|
303
450
|
});
|
|
@@ -305,15 +452,15 @@ export function useJoinCommunitySpace() {
|
|
|
305
452
|
|
|
306
453
|
/** Leave a space (POST /public/community/spaces/:id/leave). Always explicit. */
|
|
307
454
|
export function useLeaveCommunitySpace() {
|
|
308
|
-
const { client, profileId } =
|
|
455
|
+
const { client, profileId, communityId } = useCommunityScope("useLeaveCommunitySpace()");
|
|
309
456
|
const queryClient = useQueryClient();
|
|
310
457
|
return useMutation({
|
|
311
458
|
mutationFn: async (spaceId: string) => {
|
|
312
|
-
const res = await client.post(`/public/community/spaces/${spaceId}/leave`, { profileId });
|
|
459
|
+
const res = await client.post(`/public/community/spaces/${spaceId}/leave`, { profileId, communityId });
|
|
313
460
|
return { ...(res.data as SpaceMembershipResult), spaceId };
|
|
314
461
|
},
|
|
315
462
|
onSuccess: ({ spaceId, ...membership }) => {
|
|
316
|
-
setSpaceMembership(queryClient,
|
|
463
|
+
setSpaceMembership(queryClient, communityId, spaceId, membership);
|
|
317
464
|
queryClient.invalidateQueries({ queryKey: communityKeys.spaceMembers(spaceId) });
|
|
318
465
|
},
|
|
319
466
|
});
|
|
@@ -321,98 +468,135 @@ export function useLeaveCommunitySpace() {
|
|
|
321
468
|
|
|
322
469
|
/** Paginated joined-member roster for a space. */
|
|
323
470
|
export function useCommunitySpaceMembers(spaceId?: string, limit = 50) {
|
|
324
|
-
const { client, profileId } =
|
|
471
|
+
const { client, profileId, communityId } = useCommunityScope("useCommunitySpaceMembers()");
|
|
325
472
|
return useInfiniteQuery<CommunitySpaceMembersPage>({
|
|
326
473
|
queryKey: communityKeys.spaceMembers(spaceId),
|
|
327
474
|
queryFn: async ({ pageParam = 1 }) => {
|
|
328
475
|
const res = await client.get(`/public/community/spaces/${spaceId}/members`, {
|
|
329
|
-
params: { profileId, page: pageParam, limit },
|
|
476
|
+
params: { profileId, communityId, page: pageParam, limit },
|
|
330
477
|
});
|
|
331
478
|
return res.data;
|
|
332
479
|
},
|
|
333
480
|
initialPageParam: 1,
|
|
334
481
|
getNextPageParam: (lastPage) => (lastPage.hasNextPage ? lastPage.page + 1 : undefined),
|
|
335
|
-
enabled: !!spaceId && !!client && !!profileId,
|
|
482
|
+
enabled: !!spaceId && !!client && !!profileId && !!communityId,
|
|
336
483
|
});
|
|
337
484
|
}
|
|
338
485
|
|
|
339
486
|
/** Paginated posts for a space, pinned first then most recent activity. */
|
|
340
487
|
export function useCommunitySpacePosts(spaceId?: string, limit = 20) {
|
|
341
|
-
const { client, profileId } =
|
|
488
|
+
const { client, profileId, communityId } = useCommunityScope("useCommunitySpacePosts()");
|
|
342
489
|
return useInfiniteQuery<CommunityPostsPage>({
|
|
343
490
|
queryKey: communityKeys.posts(spaceId),
|
|
344
491
|
queryFn: async ({ pageParam = 1 }) => {
|
|
345
492
|
const res = await client.get(`/public/community/spaces/${spaceId}/posts`, {
|
|
346
|
-
params: { profileId, page: pageParam, limit },
|
|
493
|
+
params: { profileId, communityId, page: pageParam, limit },
|
|
347
494
|
});
|
|
348
495
|
return res.data;
|
|
349
496
|
},
|
|
350
497
|
initialPageParam: 1,
|
|
351
498
|
getNextPageParam: (lastPage) => (lastPage.hasNextPage ? lastPage.page + 1 : undefined),
|
|
352
|
-
enabled: !!spaceId && !!client && !!profileId,
|
|
499
|
+
enabled: !!spaceId && !!client && !!profileId && !!communityId,
|
|
353
500
|
});
|
|
354
501
|
}
|
|
355
502
|
|
|
356
503
|
/**
|
|
357
504
|
* A single post (GET /public/community/posts/:id). The endpoint also returns a
|
|
358
|
-
* replies page
|
|
505
|
+
* replies page: we request the minimum (limit=1) and let the dedicated
|
|
359
506
|
* replies hook own pagination, so both caches stay simple.
|
|
360
507
|
*/
|
|
361
508
|
export function useCommunityPost(postId?: string) {
|
|
362
|
-
const { client, profileId } =
|
|
509
|
+
const { client, profileId, communityId } = useCommunityScope("useCommunityPost()");
|
|
363
510
|
return useQuery<CommunityPost>({
|
|
364
511
|
queryKey: communityKeys.post(postId),
|
|
365
512
|
queryFn: async () => {
|
|
366
513
|
const res = await client.get(`/public/community/posts/${postId}`, {
|
|
367
|
-
params: { profileId, page: 1, limit: 1 },
|
|
514
|
+
params: { profileId, communityId, page: 1, limit: 1 },
|
|
368
515
|
});
|
|
369
516
|
return res.data.post;
|
|
370
517
|
},
|
|
371
|
-
enabled: !!postId && !!client && !!profileId,
|
|
518
|
+
enabled: !!postId && !!client && !!profileId && !!communityId,
|
|
372
519
|
});
|
|
373
520
|
}
|
|
374
521
|
|
|
375
522
|
/**
|
|
376
523
|
* Flat, chronological reply pages for a post (GET /public/community/posts/:id/replies).
|
|
377
|
-
* `parentReplyId` is included on every row
|
|
524
|
+
* `parentReplyId` is included on every row: assemble the tree with
|
|
378
525
|
* `buildReplyTree` (data nesting is unlimited; the 3-level display cap is a UI concern).
|
|
379
526
|
*/
|
|
380
527
|
export function useCommunityReplies(postId?: string, limit = 100) {
|
|
381
|
-
const { client, profileId } =
|
|
528
|
+
const { client, profileId, communityId } = useCommunityScope("useCommunityReplies()");
|
|
382
529
|
return useInfiniteQuery<CommunityRepliesPage>({
|
|
383
530
|
queryKey: communityKeys.replies(postId),
|
|
384
531
|
queryFn: async ({ pageParam = 1 }) => {
|
|
385
532
|
const res = await client.get(`/public/community/posts/${postId}/replies`, {
|
|
386
|
-
params: { profileId, page: pageParam, limit },
|
|
533
|
+
params: { profileId, communityId, page: pageParam, limit },
|
|
387
534
|
});
|
|
388
535
|
return res.data;
|
|
389
536
|
},
|
|
390
537
|
initialPageParam: 1,
|
|
391
538
|
getNextPageParam: (lastPage) => (lastPage.hasNextPage ? lastPage.page + 1 : undefined),
|
|
392
|
-
enabled: !!postId && !!client && !!profileId,
|
|
539
|
+
enabled: !!postId && !!client && !!profileId && !!communityId,
|
|
393
540
|
});
|
|
394
541
|
}
|
|
395
542
|
|
|
396
543
|
export interface CreateCommunityPostInput {
|
|
397
544
|
title?: string;
|
|
398
|
-
/** Lexical editor state
|
|
545
|
+
/** Lexical editor state: build one from plain text with `buildLexicalState`. */
|
|
399
546
|
body: unknown;
|
|
400
547
|
attachments?: CommunityAttachmentInput[];
|
|
401
548
|
mentionedAccountIds?: string[];
|
|
549
|
+
/** Attach a poll. `title` is then its question, and required. */
|
|
550
|
+
poll?: CommunityPollInput;
|
|
402
551
|
}
|
|
403
552
|
|
|
404
553
|
/** Create a post in a space (POST /public/community/spaces/:id/posts). */
|
|
405
554
|
export function useCreateCommunityPost(spaceId?: string) {
|
|
406
|
-
const { client, profileId } =
|
|
555
|
+
const { client, profileId, communityId } = useCommunityScope("useCreateCommunityPost()");
|
|
407
556
|
const queryClient = useQueryClient();
|
|
408
557
|
return useMutation({
|
|
409
558
|
mutationFn: async (input: CreateCommunityPostInput) => {
|
|
410
|
-
const res = await client.post(`/public/community/spaces/${spaceId}/posts`, { profileId, ...input });
|
|
559
|
+
const res = await client.post(`/public/community/spaces/${spaceId}/posts`, { profileId, communityId, ...input });
|
|
411
560
|
return res.data as CommunityPost;
|
|
412
561
|
},
|
|
413
562
|
onSuccess: (post) => {
|
|
414
563
|
prependPostToFeed(queryClient, spaceId, post);
|
|
415
|
-
queryClient.invalidateQueries({ queryKey: communityKeys.spaces(
|
|
564
|
+
queryClient.invalidateQueries({ queryKey: communityKeys.spaces(communityId) });
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export interface VoteCommunityPollInput {
|
|
570
|
+
postId: string;
|
|
571
|
+
/**
|
|
572
|
+
* The member's WHOLE answer: it replaces any earlier vote. An empty list
|
|
573
|
+
* withdraws the vote.
|
|
574
|
+
*/
|
|
575
|
+
optionIds: string[];
|
|
576
|
+
/** Cache hint: the feed the post sits in. Omitted, every loaded feed is patched. */
|
|
577
|
+
spaceId?: string;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Vote on a post's poll (POST /public/community/posts/:id/poll/vote). Answers
|
|
582
|
+
* the updated poll, which lands in the feed and post caches. The server answers
|
|
583
|
+
* 409 once the poll has closed, 400 for an option that is not on it (or several
|
|
584
|
+
* on a single-choice poll), and 403 to a muted member or one without access.
|
|
585
|
+
*/
|
|
586
|
+
export function useVoteCommunityPoll() {
|
|
587
|
+
const { client, profileId, communityId } = useCommunityScope("useVoteCommunityPoll()");
|
|
588
|
+
const queryClient = useQueryClient();
|
|
589
|
+
return useMutation({
|
|
590
|
+
mutationFn: async (input: VoteCommunityPollInput) => {
|
|
591
|
+
const res = await client.post(`/public/community/posts/${input.postId}/poll/vote`, {
|
|
592
|
+
profileId,
|
|
593
|
+
communityId,
|
|
594
|
+
optionIds: input.optionIds,
|
|
595
|
+
});
|
|
596
|
+
return { poll: res.data as CommunityPollView, input };
|
|
597
|
+
},
|
|
598
|
+
onSuccess: ({ poll, input }) => {
|
|
599
|
+
setPostPoll(queryClient, { postId: input.postId, spaceId: input.spaceId }, poll);
|
|
416
600
|
},
|
|
417
601
|
});
|
|
418
602
|
}
|
|
@@ -424,19 +608,19 @@ export interface CreateCommunityReplyInput {
|
|
|
424
608
|
mentionedAccountIds?: string[];
|
|
425
609
|
}
|
|
426
610
|
|
|
427
|
-
/** Reply to a post
|
|
611
|
+
/** Reply to a post: optionally nested under another reply. */
|
|
428
612
|
export function useCreateCommunityReply(postId?: string) {
|
|
429
|
-
const { client, profileId } =
|
|
613
|
+
const { client, profileId, communityId } = useCommunityScope("useCreateCommunityReply()");
|
|
430
614
|
const queryClient = useQueryClient();
|
|
431
615
|
return useMutation({
|
|
432
616
|
mutationFn: async (input: CreateCommunityReplyInput) => {
|
|
433
|
-
const res = await client.post(`/public/community/posts/${postId}/replies`, { profileId, ...input });
|
|
617
|
+
const res = await client.post(`/public/community/posts/${postId}/replies`, { profileId, communityId, ...input });
|
|
434
618
|
return res.data as CommunityReply;
|
|
435
619
|
},
|
|
436
620
|
onSuccess: (reply) => {
|
|
437
621
|
appendReplyToThread(queryClient, postId, reply);
|
|
438
622
|
// Replying implicitly joins the post's space server-side.
|
|
439
|
-
queryClient.invalidateQueries({ queryKey: communityKeys.spaces(
|
|
623
|
+
queryClient.invalidateQueries({ queryKey: communityKeys.spaces(communityId) });
|
|
440
624
|
},
|
|
441
625
|
});
|
|
442
626
|
}
|
|
@@ -452,7 +636,7 @@ export interface ToggleReactionResult {
|
|
|
452
636
|
|
|
453
637
|
/** Toggle an emoji reaction (POST /public/community/:entityType/:id/react). */
|
|
454
638
|
export function useToggleCommunityReaction() {
|
|
455
|
-
const { client, profileId } =
|
|
639
|
+
const { client, profileId, communityId } = useCommunityScope("useToggleCommunityReaction()");
|
|
456
640
|
const queryClient = useQueryClient();
|
|
457
641
|
return useMutation({
|
|
458
642
|
mutationFn: async (input: {
|
|
@@ -465,6 +649,7 @@ export function useToggleCommunityReaction() {
|
|
|
465
649
|
}) => {
|
|
466
650
|
const res = await client.post(`/public/community/${input.entityType}/${input.entityId}/react`, {
|
|
467
651
|
profileId,
|
|
652
|
+
communityId,
|
|
468
653
|
emoji: input.emoji,
|
|
469
654
|
});
|
|
470
655
|
return { ...(res.data as ToggleReactionResult), input };
|
|
@@ -472,14 +657,14 @@ export function useToggleCommunityReaction() {
|
|
|
472
657
|
onSuccess: ({ reactions, input }) => {
|
|
473
658
|
setReactionSummary(queryClient, input, reactions);
|
|
474
659
|
// Adding a reaction implicitly joins the space server-side.
|
|
475
|
-
queryClient.invalidateQueries({ queryKey: communityKeys.spaces(
|
|
660
|
+
queryClient.invalidateQueries({ queryKey: communityKeys.spaces(communityId) });
|
|
476
661
|
},
|
|
477
662
|
});
|
|
478
663
|
}
|
|
479
664
|
|
|
480
665
|
/** Report a post or reply to the moderators. */
|
|
481
666
|
export function useReportCommunityContent() {
|
|
482
|
-
const { client, profileId } =
|
|
667
|
+
const { client, profileId, communityId } = useCommunityScope("useReportCommunityContent()");
|
|
483
668
|
return useMutation({
|
|
484
669
|
mutationFn: async (input: {
|
|
485
670
|
entityId: string;
|
|
@@ -488,6 +673,7 @@ export function useReportCommunityContent() {
|
|
|
488
673
|
}) => {
|
|
489
674
|
const res = await client.post(`/public/community/posts/${input.entityId}/report`, {
|
|
490
675
|
profileId,
|
|
676
|
+
communityId,
|
|
491
677
|
entityType: input.entityType,
|
|
492
678
|
reason: input.reason,
|
|
493
679
|
});
|
|
@@ -496,22 +682,33 @@ export function useReportCommunityContent() {
|
|
|
496
682
|
});
|
|
497
683
|
}
|
|
498
684
|
|
|
499
|
-
/** The member's own community profile
|
|
685
|
+
/** The member's own community profile: created on first read (GET /public/community/me). */
|
|
500
686
|
export function useMyCommunityProfile(enabled = true) {
|
|
501
|
-
const { client, profileId } =
|
|
687
|
+
const { client, profileId, communityId } = useCommunityScope("useMyCommunityProfile()");
|
|
502
688
|
return useQuery<CommunityMemberProfile>({
|
|
503
|
-
queryKey: communityKeys.myProfile(
|
|
689
|
+
queryKey: communityKeys.myProfile(communityId),
|
|
504
690
|
queryFn: async () => {
|
|
505
|
-
const res = await client.get("/public/community/me", { params: { profileId } });
|
|
691
|
+
const res = await client.get("/public/community/me", { params: { profileId, communityId } });
|
|
506
692
|
return res.data;
|
|
507
693
|
},
|
|
508
|
-
enabled: enabled && !!profileId && !!client,
|
|
694
|
+
enabled: enabled && !!profileId && !!communityId && !!client,
|
|
509
695
|
});
|
|
510
696
|
}
|
|
511
697
|
|
|
698
|
+
/**
|
|
699
|
+
* Whether the signed-in member moderates THIS community (`canModerate` on
|
|
700
|
+
* `/me`), for showing the moderation tools. Display only: every moderation
|
|
701
|
+
* call is checked again by the server. Reads the same cache entry as
|
|
702
|
+
* `useMyCommunityProfile`, and stays false (with no request) until `enabled`.
|
|
703
|
+
*/
|
|
704
|
+
export function useCommunityCanModerate(enabled = true): boolean {
|
|
705
|
+
const { data } = useMyCommunityProfile(enabled);
|
|
706
|
+
return enabled && !!data?.canModerate;
|
|
707
|
+
}
|
|
708
|
+
|
|
512
709
|
/** Update display name / bio / avatar / digest cadence (PUT /public/community/me). */
|
|
513
710
|
export function useUpdateMyCommunityProfile() {
|
|
514
|
-
const { client, profileId } =
|
|
711
|
+
const { client, profileId, communityId } = useCommunityScope("useUpdateMyCommunityProfile()");
|
|
515
712
|
const queryClient = useQueryClient();
|
|
516
713
|
return useMutation({
|
|
517
714
|
mutationFn: async (input: {
|
|
@@ -519,128 +716,174 @@ export function useUpdateMyCommunityProfile() {
|
|
|
519
716
|
bio?: string | null;
|
|
520
717
|
avatarUrl?: string | null;
|
|
521
718
|
digestCadence?: CommunityDigestCadence;
|
|
719
|
+
showInDirectory?: boolean;
|
|
720
|
+
showOnMap?: boolean;
|
|
721
|
+
city?: string | null;
|
|
722
|
+
countryCode?: string | null;
|
|
522
723
|
}) => {
|
|
523
|
-
const res = await client.put("/public/community/me", { profileId, ...input });
|
|
724
|
+
const res = await client.put("/public/community/me", { profileId, communityId, ...input });
|
|
524
725
|
return res.data as CommunityMemberProfile;
|
|
525
726
|
},
|
|
526
727
|
onSuccess: (profile) => {
|
|
527
|
-
|
|
528
|
-
|
|
728
|
+
// Merged, not replaced: `/me` carries fields the PUT answer may not
|
|
729
|
+
// (`canModerate`), and dropping them would hide the moderator's tools.
|
|
730
|
+
queryClient.setQueryData(communityKeys.myProfile(communityId), (old: CommunityMemberProfile | undefined) =>
|
|
731
|
+
old ? { ...old, ...profile } : profile,
|
|
732
|
+
);
|
|
733
|
+
queryClient.invalidateQueries({ queryKey: communityKeys.member(communityId, profile?.accountId) });
|
|
734
|
+
// Directory and map visibility may have changed.
|
|
735
|
+
queryClient.invalidateQueries({ queryKey: ["public-community-directory", communityId] });
|
|
736
|
+
queryClient.invalidateQueries({ queryKey: ["public-community-member-map", communityId] });
|
|
529
737
|
},
|
|
530
738
|
});
|
|
531
739
|
}
|
|
532
740
|
|
|
533
741
|
/** Another member's profile + recent activity (filtered to spaces you can read). */
|
|
534
742
|
export function useCommunityMemberProfile(accountId?: string) {
|
|
535
|
-
const { client, profileId } =
|
|
743
|
+
const { client, profileId, communityId } = useCommunityScope("useCommunityMemberProfile()");
|
|
536
744
|
return useQuery<CommunityMemberProfile>({
|
|
537
|
-
queryKey: communityKeys.member(
|
|
745
|
+
queryKey: communityKeys.member(communityId, accountId),
|
|
538
746
|
queryFn: async () => {
|
|
539
|
-
const res = await client.get(`/public/community/profile/${accountId}`, { params: { profileId } });
|
|
747
|
+
const res = await client.get(`/public/community/profile/${accountId}`, { params: { profileId, communityId } });
|
|
540
748
|
return res.data;
|
|
541
749
|
},
|
|
542
|
-
enabled: !!accountId && !!profileId && !!client,
|
|
750
|
+
enabled: !!accountId && !!profileId && !!communityId && !!client,
|
|
543
751
|
});
|
|
544
752
|
}
|
|
545
753
|
|
|
546
754
|
/** In-app inbox (GET /public/community/notifications). Returns { data, unreadCount }. */
|
|
547
755
|
export function useCommunityNotifications({ unreadOnly = false, limit = 50 } = {}) {
|
|
548
|
-
const { client, profileId } =
|
|
756
|
+
const { client, profileId, communityId } = useCommunityScope("useCommunityNotifications()");
|
|
549
757
|
return useQuery<{ data: CommunityNotificationItem[]; unreadCount: number }>({
|
|
550
|
-
queryKey: communityKeys.notifications(
|
|
758
|
+
queryKey: communityKeys.notifications(communityId, unreadOnly),
|
|
551
759
|
queryFn: async () => {
|
|
552
760
|
const res = await client.get("/public/community/notifications", {
|
|
553
|
-
params: { profileId, page: 1, limit, unreadOnly },
|
|
761
|
+
params: { profileId, communityId, page: 1, limit, unreadOnly },
|
|
554
762
|
});
|
|
555
763
|
return res.data;
|
|
556
764
|
},
|
|
557
|
-
enabled: !!profileId && !!client,
|
|
765
|
+
enabled: !!profileId && !!communityId && !!client,
|
|
558
766
|
});
|
|
559
767
|
}
|
|
560
768
|
|
|
561
769
|
/** Unread count for the bell, polled every 30s (same cadence as chat unread counts). */
|
|
562
770
|
export function useCommunityUnreadCount(enabled = true) {
|
|
563
|
-
const { client, profileId } =
|
|
771
|
+
const { client, profileId, communityId } = useCommunityScope("useCommunityUnreadCount()");
|
|
564
772
|
return useQuery<number>({
|
|
565
|
-
queryKey: communityKeys.unread(
|
|
773
|
+
queryKey: communityKeys.unread(communityId),
|
|
566
774
|
queryFn: async () => {
|
|
567
775
|
const res = await client.get("/public/community/notifications", {
|
|
568
|
-
params: { profileId, page: 1, limit: 1, unreadOnly: true },
|
|
776
|
+
params: { profileId, communityId, page: 1, limit: 1, unreadOnly: true },
|
|
569
777
|
});
|
|
570
778
|
return res.data.unreadCount as number;
|
|
571
779
|
},
|
|
572
|
-
enabled: enabled && !!profileId && !!client,
|
|
780
|
+
enabled: enabled && !!profileId && !!communityId && !!client,
|
|
573
781
|
refetchInterval: 30000,
|
|
574
782
|
});
|
|
575
783
|
}
|
|
576
784
|
|
|
577
|
-
/** Mark notifications read
|
|
785
|
+
/** Mark notifications read: omit ids to mark ALL unread as read. */
|
|
578
786
|
export function useMarkCommunityNotificationsRead() {
|
|
579
|
-
const { client, profileId } =
|
|
787
|
+
const { client, profileId, communityId } = useCommunityScope("useMarkCommunityNotificationsRead()");
|
|
580
788
|
const queryClient = useQueryClient();
|
|
581
789
|
return useMutation({
|
|
582
790
|
mutationFn: async (ids?: string[]) => {
|
|
583
|
-
await client.post("/public/community/notifications/mark-read", { profileId, ids });
|
|
791
|
+
await client.post("/public/community/notifications/mark-read", { profileId, communityId, ids });
|
|
584
792
|
},
|
|
585
793
|
onSuccess: () => {
|
|
586
794
|
queryClient.invalidateQueries({ queryKey: ["public-community-notifications"] });
|
|
587
|
-
queryClient.invalidateQueries({ queryKey: communityKeys.unread(
|
|
795
|
+
queryClient.invalidateQueries({ queryKey: communityKeys.unread(communityId) });
|
|
588
796
|
},
|
|
589
797
|
});
|
|
590
798
|
}
|
|
591
799
|
|
|
592
800
|
/** Points leaderboard (GET /public/community/leaderboard?period=all|7|30). */
|
|
593
801
|
export function useCommunityLeaderboard(period: CommunityLeaderboardPeriod = "all", limit = 20) {
|
|
594
|
-
const { client, profileId } =
|
|
802
|
+
const { client, profileId, communityId } = useCommunityScope("useCommunityLeaderboard()");
|
|
595
803
|
return useQuery<CommunityLeaderboardEntry[]>({
|
|
596
|
-
queryKey: communityKeys.leaderboard(
|
|
804
|
+
queryKey: communityKeys.leaderboard(communityId, period),
|
|
597
805
|
queryFn: async () => {
|
|
598
|
-
const res = await client.get("/public/community/leaderboard", { params: { profileId, period, limit } });
|
|
806
|
+
const res = await client.get("/public/community/leaderboard", { params: { profileId, communityId, period, limit } });
|
|
599
807
|
return res.data;
|
|
600
808
|
},
|
|
601
|
-
enabled: !!profileId && !!client,
|
|
809
|
+
enabled: !!profileId && !!communityId && !!client,
|
|
602
810
|
});
|
|
603
811
|
}
|
|
604
812
|
|
|
813
|
+
/** How long the typed @query settles before the member search runs. */
|
|
814
|
+
export const MENTION_SEARCH_DEBOUNCE_MS = 150;
|
|
815
|
+
|
|
605
816
|
/**
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
* client-side.
|
|
817
|
+
* The member search behind @mention: the typed text as `q` (left out when
|
|
818
|
+
* blank, so a bare "@" lists the most active members), ten results, most
|
|
819
|
+
* recently active first.
|
|
610
820
|
*/
|
|
611
|
-
export function
|
|
612
|
-
const
|
|
821
|
+
export function mentionDirectoryParams(q: string | null | undefined): Record<string, string | number> {
|
|
822
|
+
const trimmed = (q ?? "").trim().slice(0, 80);
|
|
823
|
+
return { ...(trimmed ? { q: trimmed } : {}), limit: 10, sort: "active" };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Members for @mention autocomplete, searched on the server as the member
|
|
828
|
+
* types (GET /public/community/members), so every current member can be
|
|
829
|
+
* mentioned, not just the hundred with the most points. Pass the text after
|
|
830
|
+
* the "@"; pass `undefined` while no @token is active and nothing is asked
|
|
831
|
+
* for. Rows come back in the leaderboard entry shape the composers read.
|
|
832
|
+
*/
|
|
833
|
+
export function useCommunityMentionDirectory(query?: string | null) {
|
|
834
|
+
const { client, profileId, communityId } = useCommunityScope("useCommunityMentionDirectory()");
|
|
835
|
+
const active = query !== undefined && query !== null;
|
|
836
|
+
// null while no @token is open. The first text after "@" is searched at
|
|
837
|
+
// once; typing after that settles first, so a request goes per pause.
|
|
838
|
+
const [settled, setSettled] = useState<string | null>(null);
|
|
839
|
+
useEffect(() => {
|
|
840
|
+
if (!active) {
|
|
841
|
+
setSettled(null);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const id = setTimeout(() => setSettled(query ?? ""), MENTION_SEARCH_DEBOUNCE_MS);
|
|
845
|
+
return () => clearTimeout(id);
|
|
846
|
+
}, [query, active]);
|
|
847
|
+
const params = mentionDirectoryParams(settled ?? query);
|
|
613
848
|
return useQuery<CommunityLeaderboardEntry[]>({
|
|
614
|
-
queryKey:
|
|
849
|
+
queryKey: ["public-community-mention-search", communityId, params] as const,
|
|
615
850
|
queryFn: async () => {
|
|
616
|
-
const res = await client.get("/public/community/
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
851
|
+
const res = await client.get("/public/community/members", { params: { profileId, communityId, ...params } });
|
|
852
|
+
const rows = (res.data?.data ?? []) as Array<CommunityLeaderboardEntry & { points?: number }>;
|
|
853
|
+
return rows.map((m) => ({
|
|
854
|
+
accountId: m.accountId,
|
|
855
|
+
displayName: m.displayName,
|
|
856
|
+
avatarUrl: m.avatarUrl,
|
|
857
|
+
points: m.points ?? 0,
|
|
858
|
+
level: m.level,
|
|
859
|
+
firstName: m.firstName,
|
|
860
|
+
lastName: m.lastName,
|
|
861
|
+
}));
|
|
620
862
|
},
|
|
621
|
-
|
|
622
|
-
|
|
863
|
+
placeholderData: keepPreviousData,
|
|
864
|
+
enabled: active && !!profileId && !!communityId && !!client,
|
|
865
|
+
staleTime: 30_000,
|
|
623
866
|
});
|
|
624
867
|
}
|
|
625
868
|
|
|
626
869
|
/**
|
|
627
870
|
* FTS over posts + replies (min 2 chars). Scopes to spaces the member can read;
|
|
628
|
-
* pass `spaceId` (a space uuid OR its per-profile slug) to scope to one space
|
|
871
|
+
* pass `spaceId` (a space uuid OR its per-profile slug) to scope to one space:
|
|
629
872
|
* scoping can only narrow, never widen, access. Backward-compatible: called with
|
|
630
873
|
* just a query it searches every accessible space (prior behavior).
|
|
631
874
|
*/
|
|
632
875
|
export function useCommunitySearch(q: string, spaceId?: string) {
|
|
633
|
-
const { client, profileId } =
|
|
876
|
+
const { client, profileId, communityId } = useCommunityScope("useCommunitySearch()");
|
|
634
877
|
const trimmed = q.trim();
|
|
635
878
|
return useQuery<CommunitySearchResults>({
|
|
636
|
-
queryKey: communityKeys.search(
|
|
879
|
+
queryKey: communityKeys.search(communityId, trimmed, spaceId),
|
|
637
880
|
queryFn: async () => {
|
|
638
881
|
const res = await client.get("/public/community/search", {
|
|
639
|
-
params: { profileId, q: trimmed, page: 1, limit: 20, ...(spaceId ? { spaceId } : {}) },
|
|
882
|
+
params: { profileId, communityId, q: trimmed, page: 1, limit: 20, ...(spaceId ? { spaceId } : {}) },
|
|
640
883
|
});
|
|
641
884
|
return res.data;
|
|
642
885
|
},
|
|
643
|
-
enabled: trimmed.length >= 2 && !!profileId && !!client,
|
|
886
|
+
enabled: trimmed.length >= 2 && !!profileId && !!communityId && !!client,
|
|
644
887
|
});
|
|
645
888
|
}
|
|
646
889
|
|
|
@@ -714,6 +957,32 @@ export function bumpFeedReplyCount(queryClient: QueryClient, spaceId: string | u
|
|
|
714
957
|
});
|
|
715
958
|
}
|
|
716
959
|
|
|
960
|
+
/**
|
|
961
|
+
* Put a post's updated poll into every cache that renders the post: its feed
|
|
962
|
+
* (the one named, or every loaded feed when none is) and the single-post read.
|
|
963
|
+
*/
|
|
964
|
+
export function setPostPoll(
|
|
965
|
+
queryClient: QueryClient,
|
|
966
|
+
target: { postId: string; spaceId?: string },
|
|
967
|
+
poll: CommunityPollView | null,
|
|
968
|
+
) {
|
|
969
|
+
const patchFeed = (old: any) => {
|
|
970
|
+
if (!old?.pages?.length) return old;
|
|
971
|
+
return {
|
|
972
|
+
...old,
|
|
973
|
+
pages: old.pages.map((page: any) => ({
|
|
974
|
+
...page,
|
|
975
|
+
data: page.data.map((p: CommunityPost) => (p.id === target.postId ? { ...p, poll } : p)),
|
|
976
|
+
})),
|
|
977
|
+
};
|
|
978
|
+
};
|
|
979
|
+
if (target.spaceId) queryClient.setQueryData(communityKeys.posts(target.spaceId), patchFeed);
|
|
980
|
+
else queryClient.setQueriesData({ queryKey: ["public-community-posts"] }, patchFeed);
|
|
981
|
+
queryClient.setQueryData(communityKeys.post(target.postId), (old: CommunityPost | undefined) =>
|
|
982
|
+
old ? { ...old, poll } : old,
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
717
986
|
/** Replace an entity's reaction summary in every cache that renders it. */
|
|
718
987
|
export function setReactionSummary(
|
|
719
988
|
queryClient: QueryClient,
|
|
@@ -792,3 +1061,82 @@ export function applyReactionDelta(
|
|
|
792
1061
|
});
|
|
793
1062
|
}
|
|
794
1063
|
}
|
|
1064
|
+
|
|
1065
|
+
/** The `communityContentHeld` socket payload: auto-moderation sent it back to the approval queue. */
|
|
1066
|
+
export interface CommunityContentHeldEvent {
|
|
1067
|
+
spaceId: string;
|
|
1068
|
+
entityType: "community_post" | "community_reply";
|
|
1069
|
+
postId: string | null;
|
|
1070
|
+
replyId: string | null;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Take a post or reply that was just held for approval out of the open feeds
|
|
1075
|
+
* and threads. Everyone else loses it at once; its author keeps it, marked as
|
|
1076
|
+
* waiting for approval, exactly as the server shows them their own held work.
|
|
1077
|
+
* An open post that is held for a reader who is not its author becomes `null`,
|
|
1078
|
+
* which the thread renders as unavailable.
|
|
1079
|
+
*/
|
|
1080
|
+
export function applyCommunityContentHeld(
|
|
1081
|
+
queryClient: QueryClient,
|
|
1082
|
+
event: CommunityContentHeldEvent,
|
|
1083
|
+
viewerAccountId: string | null | undefined,
|
|
1084
|
+
) {
|
|
1085
|
+
const mine = (authorAccountId: string | null | undefined) => !!viewerAccountId && authorAccountId === viewerAccountId;
|
|
1086
|
+
const asPending = <T extends { status?: CommunityContentStatus }>(item: T): T => ({ ...item, status: "pending" });
|
|
1087
|
+
|
|
1088
|
+
if (event.entityType === "community_post" && event.postId) {
|
|
1089
|
+
const postId = event.postId;
|
|
1090
|
+
queryClient.setQueryData(communityKeys.posts(event.spaceId), (old: any) => {
|
|
1091
|
+
if (!old?.pages?.length) return old;
|
|
1092
|
+
return {
|
|
1093
|
+
...old,
|
|
1094
|
+
pages: old.pages.map((page: any) => {
|
|
1095
|
+
const had = page.data.some((p: CommunityPost) => p.id === postId);
|
|
1096
|
+
if (!had) return page;
|
|
1097
|
+
const data = page.data.flatMap((p: CommunityPost) =>
|
|
1098
|
+
p.id !== postId ? [p] : mine(p.authorAccountId) ? [asPending(p)] : [],
|
|
1099
|
+
);
|
|
1100
|
+
return { ...page, data, total: Math.max(0, page.total - (page.data.length - data.length)) };
|
|
1101
|
+
}),
|
|
1102
|
+
};
|
|
1103
|
+
});
|
|
1104
|
+
queryClient.setQueryData(communityKeys.post(postId), (old: CommunityPost | null | undefined) =>
|
|
1105
|
+
old ? (mine(old.authorAccountId) ? asPending(old) : null) : old,
|
|
1106
|
+
);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (event.entityType === "community_reply" && event.postId && event.replyId) {
|
|
1111
|
+
const { postId, replyId } = event;
|
|
1112
|
+
let removed = false;
|
|
1113
|
+
queryClient.setQueryData(communityKeys.replies(postId), (old: any) => {
|
|
1114
|
+
if (!old?.pages?.length) return old;
|
|
1115
|
+
return {
|
|
1116
|
+
...old,
|
|
1117
|
+
pages: old.pages.map((page: any) => {
|
|
1118
|
+
if (!page.data.some((r: CommunityReply) => r.id === replyId)) return page;
|
|
1119
|
+
const data = page.data.flatMap((r: CommunityReply) =>
|
|
1120
|
+
r.id !== replyId ? [r] : mine(r.authorAccountId) ? [asPending(r)] : [],
|
|
1121
|
+
);
|
|
1122
|
+
if (data.length < page.data.length) removed = true;
|
|
1123
|
+
return { ...page, data, total: Math.max(0, page.total - (page.data.length - data.length)) };
|
|
1124
|
+
}),
|
|
1125
|
+
};
|
|
1126
|
+
});
|
|
1127
|
+
if (removed) {
|
|
1128
|
+
const drop = (p: CommunityPost) => ({ ...p, replyCount: Math.max(0, (p.replyCount ?? 0) - 1) });
|
|
1129
|
+
queryClient.setQueryData(communityKeys.post(postId), (old: CommunityPost | null | undefined) => (old ? drop(old) : old));
|
|
1130
|
+
queryClient.setQueryData(communityKeys.posts(event.spaceId), (old: any) => {
|
|
1131
|
+
if (!old?.pages?.length) return old;
|
|
1132
|
+
return {
|
|
1133
|
+
...old,
|
|
1134
|
+
pages: old.pages.map((page: any) => ({
|
|
1135
|
+
...page,
|
|
1136
|
+
data: page.data.map((p: CommunityPost) => (p.id === postId ? drop(p) : p)),
|
|
1137
|
+
})),
|
|
1138
|
+
};
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|