@tribe-nest/forge 3.60.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/contexts/CartContext.tsx +34 -1
- 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 +10 -1
- package/src/data/queries/useCheckouts.ts +2 -1
- package/src/data/queries/useCoachingAvailability.ts +10 -1
- package/src/data/queries/useCommunities.ts +2 -0
- package/src/data/queries/useCommunity.ts +362 -18
- 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/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/useSiteApp.ts +130 -0
- package/src/data/queries/useSubscriptions.ts +6 -1
- package/src/i18n/de.json +746 -1
- package/src/i18n/en.json +746 -1
- package/src/index.ts +17 -0
- package/src/runtime/pages.spec.ts +5 -0
- package/src/runtime/pages.ts +32 -6
- 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/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/communityHarness.tsx +10 -3
- 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/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/useCommunityJoin.spec.tsx +228 -14
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +29 -1
- package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +11 -2
- 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 +20 -1
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
- package/src/ui/headless/chat/useChatRoom.ts +35 -1
- package/src/ui/headless/communities/index.ts +41 -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 +3 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +73 -23
- package/src/ui/headless/communities/useCommunityMembership.ts +48 -2
- package/src/ui/headless/communities/useCommunityWiki.ts +266 -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/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 +13 -0
- package/src/ui/headless/memberHome/index.ts +10 -0
- package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +56 -18
- 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/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 +11 -0
- package/src/ui/styled/AccountDashboard.tsx +712 -352
- 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/MusicLinkPage.tsx +5 -5
- 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 +5 -5
- package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
- package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
- 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/CommunityAbout.tsx +6 -1
- 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 +134 -11
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +119 -19
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +168 -52
- package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
- package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +31 -5
- 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/CommunityWiki.spec.tsx +223 -0
- package/src/ui/styled/communities/index.ts +9 -0
- package/src/ui/styled/communities/shared.tsx +39 -11
- 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/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/index.ts +37 -2
- package/src/ui/styled/forge-utilities.css +234 -0
- package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +13 -158
- package/src/ui/styled/members/MemberHomeLayout.tsx +295 -15
- package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
- 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/landing.ts +19 -6
- package/src/utils/metaEvents.ts +302 -0
|
@@ -5,15 +5,29 @@ import { useCommunityThread } from "../../headless/community/useCommunityThread"
|
|
|
5
5
|
import { findReplyNode, type ReplyNode } from "../../headless/community/replyTree";
|
|
6
6
|
import {
|
|
7
7
|
communityMemberName,
|
|
8
|
+
isCommunityContentPending,
|
|
8
9
|
useReportCommunityContent,
|
|
9
10
|
type CommunityReply,
|
|
10
11
|
} from "../../../data/queries/useCommunity";
|
|
11
12
|
import { Loading } from "../Loading";
|
|
12
13
|
import { CommunityComposer } from "./CommunityComposer";
|
|
13
14
|
import { CommunityMediaGallery } from "./CommunityMediaGallery";
|
|
15
|
+
import { CommunityPollCard } from "./CommunityPollCard";
|
|
16
|
+
import { CommunityLinkPreview } from "./CommunityLinkPreview";
|
|
17
|
+
import { CommunityModeratorMenu } from "./CommunityModeratorMenu";
|
|
18
|
+
import { CommunityPendingBadge, CommunityRemovedNotice } from "./CommunityModerationState";
|
|
14
19
|
import { CommunityReactionBar } from "./CommunityReactionBar";
|
|
15
20
|
import { CommunityRichText } from "./CommunityRichText";
|
|
16
21
|
import { LevelBadge, MemberAvatar } from "./MemberAvatar";
|
|
22
|
+
import {
|
|
23
|
+
CommunityAgentAvatar,
|
|
24
|
+
CommunityAgentFeedback,
|
|
25
|
+
CommunityAgentName,
|
|
26
|
+
CommunityAgentThinking,
|
|
27
|
+
CommunityCatchMeUp,
|
|
28
|
+
} from "./CommunityAgent";
|
|
29
|
+
import { useCommunityAgent } from "../../../data/queries/useCommunityAgent";
|
|
30
|
+
import { CATCH_ME_UP_MIN_REPLIES, useCommunityAgentAnswerRatings } from "../../headless/community/useCommunityAgent";
|
|
17
31
|
import { timeAgo } from "./util";
|
|
18
32
|
|
|
19
33
|
/** How many nesting levels are RENDERED before "Continue this thread". The
|
|
@@ -33,6 +47,14 @@ export function CommunityPostDetail({ postId, onOpenProfile, onBack }: Community
|
|
|
33
47
|
const t = useThemeTokens();
|
|
34
48
|
const thread = useCommunityThread(postId);
|
|
35
49
|
const report = useReportCommunityContent();
|
|
50
|
+
// The community AI: the "Catch me up" panel and the name on its thinking line.
|
|
51
|
+
const { agent } = useCommunityAgent();
|
|
52
|
+
// The viewer's own ratings on the AI answers here: who may rate, and how they did.
|
|
53
|
+
const agentAnswerIds = useMemo(
|
|
54
|
+
() => thread.replies.filter((r) => r.agentAuthor).map((r) => r.id),
|
|
55
|
+
[thread.replies],
|
|
56
|
+
);
|
|
57
|
+
const ratings = useCommunityAgentAnswerRatings("community_reply", agentAnswerIds);
|
|
36
58
|
const border = `1px solid ${t.border}`;
|
|
37
59
|
|
|
38
60
|
// "Continue thread" re-roots the rendered tree on a deep reply (Reddit-style).
|
|
@@ -58,6 +80,8 @@ export function CommunityPostDetail({ postId, onOpenProfile, onBack }: Community
|
|
|
58
80
|
|
|
59
81
|
const post = thread.post;
|
|
60
82
|
const locked = post.isLocked;
|
|
83
|
+
// The author's own held post: no replies or reactions until it is approved.
|
|
84
|
+
const pending = isCommunityContentPending(post);
|
|
61
85
|
|
|
62
86
|
const doReport = (entityId: string, entityType: "community_post" | "community_reply") => {
|
|
63
87
|
if (reported.has(entityId)) return;
|
|
@@ -106,32 +130,37 @@ export function CommunityPostDetail({ postId, onOpenProfile, onBack }: Community
|
|
|
106
130
|
const renderNode = (node: ReplyNode, renderedDepth: number): React.ReactNode => {
|
|
107
131
|
const reply = node.reply;
|
|
108
132
|
const capped = renderedDepth >= REPLY_DISPLAY_DEPTH - 1 && node.children.length > 0;
|
|
133
|
+
const replyPending = isCommunityContentPending(reply);
|
|
109
134
|
|
|
110
135
|
return (
|
|
111
136
|
<div key={reply.id} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
112
137
|
<ReplyItem
|
|
113
138
|
reply={reply}
|
|
139
|
+
showFeedback={!!reply.agentAuthor && ratings.canRate(reply.id)}
|
|
140
|
+
savedRating={ratings.ratingOf(reply.id)}
|
|
114
141
|
onOpenProfile={onOpenProfile}
|
|
115
142
|
onReact={(emoji) => thread.react("community_reply", reply.id, emoji)}
|
|
116
143
|
extraActions={
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
144
|
+
replyPending ? null : (
|
|
145
|
+
<>
|
|
146
|
+
{!locked && (
|
|
147
|
+
<button
|
|
148
|
+
onClick={() => setReplyingTo((prev) => (prev === reply.id ? null : reply.id))}
|
|
149
|
+
style={{
|
|
150
|
+
background: "none",
|
|
151
|
+
border: "none",
|
|
152
|
+
cursor: "pointer",
|
|
153
|
+
color: t.muted,
|
|
154
|
+
fontSize: 12,
|
|
155
|
+
padding: "2px 4px",
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
Reply
|
|
159
|
+
</button>
|
|
160
|
+
)}
|
|
161
|
+
{reportButton(reply.id, "community_reply")}
|
|
162
|
+
</>
|
|
163
|
+
)
|
|
135
164
|
}
|
|
136
165
|
/>
|
|
137
166
|
|
|
@@ -143,7 +172,7 @@ export function CommunityPostDetail({ postId, onOpenProfile, onBack }: Community
|
|
|
143
172
|
<CommunityComposer
|
|
144
173
|
compact
|
|
145
174
|
autoFocus
|
|
146
|
-
placeholder={`Reply to ${communityMemberName(reply.author)}…`}
|
|
175
|
+
placeholder={`Reply to ${reply.agentAuthor ? reply.agentAuthor.name : communityMemberName(reply.author)}…`}
|
|
147
176
|
submitLabel="Reply"
|
|
148
177
|
onSubmit={submitReply}
|
|
149
178
|
onCancel={() => setReplyingTo(null)}
|
|
@@ -198,48 +227,69 @@ export function CommunityPostDetail({ postId, onOpenProfile, onBack }: Community
|
|
|
198
227
|
</button>
|
|
199
228
|
)}
|
|
200
229
|
|
|
201
|
-
{/* Post */}
|
|
202
|
-
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
<
|
|
230
|
+
{/* Post. Just removed by this moderator: the undo, in its place. */}
|
|
231
|
+
{post.archivedAt ? (
|
|
232
|
+
<CommunityRemovedNotice target={{ type: "post", post }} />
|
|
233
|
+
) : (
|
|
234
|
+
<article style={{ border, borderRadius: t.cornerRadius, background: t.surface, padding: 18 }}>
|
|
235
|
+
<header style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
236
|
+
{post.agentAuthor ? (
|
|
237
|
+
<CommunityAgentAvatar agent={post.agentAuthor} size={40} />
|
|
238
|
+
) : (
|
|
239
|
+
<MemberAvatar member={post.author} size={40} onClick={() => post.author && onOpenProfile?.(post.author.accountId)} />
|
|
240
|
+
)}
|
|
241
|
+
<div style={{ minWidth: 0, flex: 1 }}>
|
|
242
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
|
243
|
+
{post.agentAuthor ? (
|
|
244
|
+
<CommunityAgentName agent={post.agentAuthor} fontSize={15} fontWeight={700} />
|
|
245
|
+
) : (
|
|
246
|
+
<>
|
|
247
|
+
<button
|
|
248
|
+
onClick={() => post.author && onOpenProfile?.(post.author.accountId)}
|
|
249
|
+
style={{ background: "none", border: "none", padding: 0, cursor: "pointer", color: t.text, fontWeight: 700, fontSize: 15 }}
|
|
250
|
+
>
|
|
251
|
+
{communityMemberName(post.author)}
|
|
252
|
+
</button>
|
|
253
|
+
<LevelBadge level={post.author?.level} />
|
|
254
|
+
</>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
<div style={{ fontSize: 12, color: t.muted }}>{timeAgo(post.createdAt)}</div>
|
|
214
258
|
</div>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
259
|
+
{pending && <CommunityPendingBadge />}
|
|
260
|
+
{locked && (
|
|
261
|
+
<span style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 12, color: t.muted }}>
|
|
262
|
+
<Lock size={12} /> Locked
|
|
263
|
+
</span>
|
|
264
|
+
)}
|
|
265
|
+
<CommunityModeratorMenu target={{ type: "post", post }} />
|
|
266
|
+
</header>
|
|
223
267
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
268
|
+
{post.title && (
|
|
269
|
+
<h2 style={{ margin: "12px 0 6px", fontSize: 20, fontFamily: t.headingFontFamily, color: t.text }}>{post.title}</h2>
|
|
270
|
+
)}
|
|
271
|
+
<div style={{ marginTop: post.title ? 0 : 12 }}>
|
|
272
|
+
<CommunityRichText body={post.body} linkify={!!post.agentAuthor} />
|
|
273
|
+
</div>
|
|
274
|
+
<CommunityMediaGallery media={post.media ?? []} />
|
|
275
|
+
<CommunityLinkPreview preview={post.linkPreview} />
|
|
276
|
+
{post.poll && <CommunityPollCard post={post} disabled={pending} />}
|
|
231
277
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
278
|
+
{!pending && (
|
|
279
|
+
<footer style={{ marginTop: 14 }}>
|
|
280
|
+
<CommunityReactionBar reactions={post.reactions ?? []} onToggle={(emoji) => thread.react("community_post", post.id, emoji)}>
|
|
281
|
+
{reportButton(post.id, "community_post")}
|
|
282
|
+
</CommunityReactionBar>
|
|
283
|
+
</footer>
|
|
284
|
+
)}
|
|
285
|
+
</article>
|
|
286
|
+
)}
|
|
238
287
|
|
|
239
288
|
{/* Top-level composer: closed until asked for, and hidden rather than
|
|
240
289
|
unmounted while a nested reply box is open, so a half-written
|
|
241
|
-
top-level reply survives the detour of replying to someone else.
|
|
242
|
-
|
|
290
|
+
top-level reply survives the detour of replying to someone else.
|
|
291
|
+
None at all on a held or just-removed post. */}
|
|
292
|
+
{pending || post.archivedAt ? null : locked ? (
|
|
243
293
|
<div style={{ border, borderRadius: t.cornerRadius, padding: 14, textAlign: "center", color: t.muted, fontSize: 14 }}>
|
|
244
294
|
<Lock size={13} style={{ verticalAlign: -2, marginRight: 6 }} />
|
|
245
295
|
This post is locked: replies are closed.
|
|
@@ -273,6 +323,11 @@ export function CommunityPostDetail({ postId, onOpenProfile, onBack }: Community
|
|
|
273
323
|
)}
|
|
274
324
|
</div>
|
|
275
325
|
|
|
326
|
+
{/* A private summary of a long thread, for this member only. */}
|
|
327
|
+
{agent && !pending && !post.archivedAt && post.replyCount >= CATCH_ME_UP_MIN_REPLIES && (
|
|
328
|
+
<CommunityCatchMeUp postId={post.id} agent={agent} />
|
|
329
|
+
)}
|
|
330
|
+
|
|
276
331
|
{thread.repliesLoading && !thread.replies.length ? (
|
|
277
332
|
<Loading />
|
|
278
333
|
) : (
|
|
@@ -281,6 +336,8 @@ export function CommunityPostDetail({ postId, onOpenProfile, onBack }: Community
|
|
|
281
336
|
</div>
|
|
282
337
|
)}
|
|
283
338
|
|
|
339
|
+
{thread.agentThinking && <CommunityAgentThinking name={agent?.name} />}
|
|
340
|
+
|
|
284
341
|
{thread.hasMore && (
|
|
285
342
|
<button
|
|
286
343
|
onClick={() => thread.fetchNextPage()}
|
|
@@ -312,39 +369,68 @@ function ReplyItem({
|
|
|
312
369
|
onOpenProfile,
|
|
313
370
|
onReact,
|
|
314
371
|
extraActions,
|
|
372
|
+
showFeedback = false,
|
|
373
|
+
savedRating = null,
|
|
315
374
|
}: {
|
|
316
375
|
reply: CommunityReply;
|
|
317
376
|
onOpenProfile?: (accountId: string) => void;
|
|
318
377
|
onReact: (emoji: string) => void;
|
|
319
378
|
extraActions?: React.ReactNode;
|
|
379
|
+
/** An AI answer to this viewer's own question: offer thumbs up / down. */
|
|
380
|
+
showFeedback?: boolean;
|
|
381
|
+
/** Their saved rating on it. */
|
|
382
|
+
savedRating?: 1 | -1 | null;
|
|
320
383
|
}) {
|
|
321
384
|
const t = useThemeTokens();
|
|
385
|
+
const pending = isCommunityContentPending(reply);
|
|
386
|
+
// The community AI wrote it: its name and badge, never the "Member" fallback.
|
|
387
|
+
const agent = reply.agentAuthor ?? null;
|
|
388
|
+
|
|
389
|
+
// Just removed by this moderator: the undo, in its place. Its children stay
|
|
390
|
+
// visible beneath it, as the thread renders them.
|
|
391
|
+
if (reply.archivedAt) return <CommunityRemovedNotice target={{ type: "reply", reply }} />;
|
|
322
392
|
|
|
323
393
|
return (
|
|
324
|
-
<div style={{ display: "flex", gap: 10 }}>
|
|
325
|
-
|
|
394
|
+
<div style={{ display: "flex", gap: 10 }} data-testid={agent ? "community-agent-reply" : undefined}>
|
|
395
|
+
{agent ? (
|
|
396
|
+
<CommunityAgentAvatar agent={agent} size={32} />
|
|
397
|
+
) : (
|
|
398
|
+
<MemberAvatar member={reply.author} size={32} onClick={() => reply.author && onOpenProfile?.(reply.author.accountId)} />
|
|
399
|
+
)}
|
|
326
400
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
327
|
-
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
401
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
|
|
402
|
+
{agent ? (
|
|
403
|
+
<CommunityAgentName agent={agent} fontSize={13} />
|
|
404
|
+
) : (
|
|
405
|
+
<>
|
|
406
|
+
<button
|
|
407
|
+
onClick={() => reply.author && onOpenProfile?.(reply.author.accountId)}
|
|
408
|
+
style={{ background: "none", border: "none", padding: 0, cursor: "pointer", color: t.text, fontWeight: 600, fontSize: 13 }}
|
|
409
|
+
>
|
|
410
|
+
{communityMemberName(reply.author)}
|
|
411
|
+
</button>
|
|
412
|
+
<LevelBadge level={reply.author?.level} />
|
|
413
|
+
</>
|
|
414
|
+
)}
|
|
335
415
|
<span style={{ fontSize: 11, color: t.muted }}>{timeAgo(reply.createdAt)}</span>
|
|
416
|
+
{pending && <CommunityPendingBadge />}
|
|
417
|
+
<CommunityModeratorMenu target={{ type: "reply", reply }} className="fg:ml-auto" />
|
|
336
418
|
</div>
|
|
337
419
|
|
|
338
420
|
<div style={{ marginTop: 2 }}>
|
|
339
|
-
<CommunityRichText body={reply.body} />
|
|
421
|
+
<CommunityRichText body={reply.body} linkify={!!agent} />
|
|
340
422
|
</div>
|
|
341
423
|
<CommunityMediaGallery media={reply.media ?? []} />
|
|
342
424
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
425
|
+
{/* A held reply takes no reactions until it is approved. */}
|
|
426
|
+
{!pending && (
|
|
427
|
+
<div style={{ marginTop: 6 }}>
|
|
428
|
+
<CommunityReactionBar reactions={reply.reactions ?? []} onToggle={onReact}>
|
|
429
|
+
{extraActions}
|
|
430
|
+
{showFeedback && <CommunityAgentFeedback entityType="community_reply" entityId={reply.id} saved={savedRating} />}
|
|
431
|
+
</CommunityReactionBar>
|
|
432
|
+
</div>
|
|
433
|
+
)}
|
|
348
434
|
</div>
|
|
349
435
|
</div>
|
|
350
436
|
);
|
|
@@ -1,24 +1,64 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
1
2
|
import { useThemeTokens } from "../../theme/ForgeThemeProvider";
|
|
2
3
|
import type { LexicalNodeJson } from "../../headless/community/lexical";
|
|
4
|
+
import { lexicalNodeText, parseWikiContent, wikiImageSource } from "../../headless/communities/useCommunityWiki";
|
|
5
|
+
import { linkifyText } from "../../headless/community/useCommunityAgent";
|
|
6
|
+
|
|
7
|
+
/** A private Knowledge Base image (`kbmedia:<id>`) handed to the caller to resolve. */
|
|
8
|
+
export interface CommunityRichTextPrivateImage {
|
|
9
|
+
mediaId: string;
|
|
10
|
+
alt: string;
|
|
11
|
+
/** Sits inside a paragraph rather than on its own line. */
|
|
12
|
+
inline: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Set only by a caller that can resolve private images (the wiki page view). */
|
|
16
|
+
const PrivateImageContext = createContext<((image: CommunityRichTextPrivateImage) => ReactNode) | null>(null);
|
|
17
|
+
|
|
18
|
+
/** Set for plain-text bodies whose addresses should be clickable (the community AI's answers). */
|
|
19
|
+
const LinkifyContext = createContext(false);
|
|
3
20
|
|
|
4
21
|
export interface CommunityRichTextProps {
|
|
5
|
-
/** Lexical editor state JSON (community_posts.body / community_replies.body). */
|
|
22
|
+
/** Lexical editor state JSON (community_posts.body / community_replies.body, or a wiki page). */
|
|
6
23
|
body: unknown;
|
|
7
24
|
/** Truncate to a preview (clamps lines via CSS). */
|
|
8
25
|
clampLines?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Anchor ids for the top-level headings, in document order, skipping empty
|
|
28
|
+
* ones (what `extractWikiToc` returns), so a table of contents can link to them.
|
|
29
|
+
*/
|
|
30
|
+
headingIds?: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Renders a PRIVATE Knowledge Base image (`kbmedia:<id>`). Only the wiki page
|
|
33
|
+
* view passes it, because only it can ask for a signed address. Without it a
|
|
34
|
+
* private image renders nothing; a plain http(s) image always renders.
|
|
35
|
+
*/
|
|
36
|
+
renderPrivateImage?: (image: CommunityRichTextPrivateImage) => ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* Make bare http(s) addresses in the text clickable and keep line breaks.
|
|
39
|
+
* For the community AI's answers, written as plain text with a "Sources:"
|
|
40
|
+
* list of addresses; a member's own text is never rewritten.
|
|
41
|
+
*/
|
|
42
|
+
linkify?: boolean;
|
|
9
43
|
}
|
|
10
44
|
|
|
11
45
|
/**
|
|
12
|
-
* Dependency-free renderer for
|
|
13
|
-
* Handles the node set our
|
|
14
|
-
* plus the
|
|
15
|
-
* (heading
|
|
16
|
-
* children/text so content
|
|
46
|
+
* Dependency-free renderer for Lexical states: community posts and replies,
|
|
47
|
+
* and wiki pages written in the Knowledge Base editor. Handles the node set our
|
|
48
|
+
* composer emits (paragraph/text/mention/linebreak) plus the rich nodes the
|
|
49
|
+
* Knowledge Base editor produces (heading, quote, list, link, code, divider,
|
|
50
|
+
* table, image). Unknown nodes fall back to their children/text so content
|
|
51
|
+
* never disappears.
|
|
17
52
|
*/
|
|
18
|
-
export function CommunityRichText({ body, clampLines }: CommunityRichTextProps) {
|
|
53
|
+
export function CommunityRichText({ body, clampLines, headingIds, renderPrivateImage, linkify = false }: CommunityRichTextProps) {
|
|
19
54
|
const t = useThemeTokens();
|
|
20
|
-
const root =
|
|
55
|
+
const root = parseWikiContent(body)?.root ?? null;
|
|
21
56
|
if (!root) return null;
|
|
57
|
+
let headingIndex = 0;
|
|
58
|
+
const anchorFor = (node: LexicalNodeJson): string | undefined => {
|
|
59
|
+
if (!headingIds || node.type !== "heading" || !lexicalNodeText(node).trim()) return undefined;
|
|
60
|
+
return headingIds[headingIndex++];
|
|
61
|
+
};
|
|
22
62
|
|
|
23
63
|
const clampStyle: React.CSSProperties = clampLines
|
|
24
64
|
? {
|
|
@@ -30,23 +70,63 @@ export function CommunityRichText({ body, clampLines }: CommunityRichTextProps)
|
|
|
30
70
|
: {};
|
|
31
71
|
|
|
32
72
|
return (
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
73
|
+
<PrivateImageContext.Provider value={renderPrivateImage ?? null}>
|
|
74
|
+
<LinkifyContext.Provider value={linkify}>
|
|
75
|
+
<div
|
|
76
|
+
style={{
|
|
77
|
+
color: t.text,
|
|
78
|
+
fontSize: 15,
|
|
79
|
+
lineHeight: 1.55,
|
|
80
|
+
wordBreak: "break-word",
|
|
81
|
+
...(linkify ? { whiteSpace: "pre-line" as const } : {}),
|
|
82
|
+
...clampStyle,
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{(root.children ?? []).map((node, i) => (
|
|
86
|
+
<BlockNode key={i} node={node} anchorId={anchorFor(node)} />
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
</LinkifyContext.Provider>
|
|
90
|
+
</PrivateImageContext.Provider>
|
|
38
91
|
);
|
|
39
92
|
}
|
|
40
93
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
94
|
+
/**
|
|
95
|
+
* A Knowledge Base image. A plain http(s) address renders as it is; a private
|
|
96
|
+
* `kbmedia:` reference goes to the caller's resolver, or renders nothing; any
|
|
97
|
+
* other `src` never renders.
|
|
98
|
+
*/
|
|
99
|
+
function RichImage({ node, inline }: { node: LexicalNodeJson; inline: boolean }) {
|
|
100
|
+
const t = useThemeTokens();
|
|
101
|
+
const renderPrivate = useContext(PrivateImageContext);
|
|
102
|
+
const source = wikiImageSource(node.src);
|
|
103
|
+
const alt = typeof node.altText === "string" ? node.altText : "";
|
|
104
|
+
if (!source) return null;
|
|
105
|
+
if (source.kind === "private")
|
|
106
|
+
return renderPrivate ? <>{renderPrivate({ mediaId: source.mediaId, alt, inline })}</> : null;
|
|
107
|
+
return (
|
|
108
|
+
<img
|
|
109
|
+
src={source.url}
|
|
110
|
+
alt={alt}
|
|
111
|
+
loading="lazy"
|
|
112
|
+
style={
|
|
113
|
+
inline
|
|
114
|
+
? { maxWidth: "100%", height: "auto", borderRadius: t.cornerRadius, verticalAlign: "middle" }
|
|
115
|
+
: { display: "block", maxWidth: "100%", height: "auto", margin: "0.6em 0", borderRadius: t.cornerRadius }
|
|
116
|
+
}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Only web and mail links render as links; anything else (`javascript:` and friends) is dropped. */
|
|
122
|
+
function safeHref(url: unknown): string | null {
|
|
123
|
+
if (typeof url !== "string" || !url.trim()) return null;
|
|
124
|
+
const value = url.trim();
|
|
125
|
+
if (/^(https?:|mailto:|tel:)/i.test(value) || value.startsWith("/") || value.startsWith("#")) return value;
|
|
46
126
|
return null;
|
|
47
127
|
}
|
|
48
128
|
|
|
49
|
-
function BlockNode({ node }: { node: LexicalNodeJson }) {
|
|
129
|
+
function BlockNode({ node, anchorId }: { node: LexicalNodeJson; anchorId?: string }) {
|
|
50
130
|
const t = useThemeTokens();
|
|
51
131
|
const children = (node.children ?? []).map((child, i) => <InlineNode key={i} node={child} />);
|
|
52
132
|
|
|
@@ -62,7 +142,47 @@ function BlockNode({ node }: { node: LexicalNodeJson }) {
|
|
|
62
142
|
| "h4"
|
|
63
143
|
| "h5"
|
|
64
144
|
| "h6";
|
|
65
|
-
return
|
|
145
|
+
return (
|
|
146
|
+
<Tag id={anchorId} style={{ margin: "0.4em 0", fontFamily: t.headingFontFamily, scrollMarginTop: 16 }}>
|
|
147
|
+
{children}
|
|
148
|
+
</Tag>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
case "horizontalrule":
|
|
152
|
+
return <hr style={{ border: "none", borderTop: `1px solid ${t.border}`, margin: "1em 0" }} />;
|
|
153
|
+
case "kb-image":
|
|
154
|
+
case "image":
|
|
155
|
+
return <RichImage node={node} inline={false} />;
|
|
156
|
+
case "table":
|
|
157
|
+
return (
|
|
158
|
+
<div style={{ overflowX: "auto", margin: "0.6em 0" }}>
|
|
159
|
+
<table style={{ borderCollapse: "collapse", minWidth: "100%", fontSize: 14 }}>
|
|
160
|
+
<tbody>
|
|
161
|
+
{(node.children ?? []).map((row, i) => (
|
|
162
|
+
<BlockNode key={i} node={row} />
|
|
163
|
+
))}
|
|
164
|
+
</tbody>
|
|
165
|
+
</table>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
case "tablerow":
|
|
169
|
+
return (
|
|
170
|
+
<tr>
|
|
171
|
+
{(node.children ?? []).map((cell, i) => (
|
|
172
|
+
<BlockNode key={i} node={cell} />
|
|
173
|
+
))}
|
|
174
|
+
</tr>
|
|
175
|
+
);
|
|
176
|
+
case "tablecell": {
|
|
177
|
+
const header = typeof node.headerState === "number" && node.headerState > 0;
|
|
178
|
+
const Cell = header ? "th" : "td";
|
|
179
|
+
return (
|
|
180
|
+
<Cell style={{ border: `1px solid ${t.border}`, padding: "6px 10px", textAlign: "left", verticalAlign: "top" }}>
|
|
181
|
+
{(node.children ?? []).map((child, i) => (
|
|
182
|
+
<BlockNode key={i} node={child} />
|
|
183
|
+
))}
|
|
184
|
+
</Cell>
|
|
185
|
+
);
|
|
66
186
|
}
|
|
67
187
|
case "quote":
|
|
68
188
|
return (
|
|
@@ -76,7 +196,14 @@ function BlockNode({ node }: { node: LexicalNodeJson }) {
|
|
|
76
196
|
return <ListTag style={{ margin: "0.4em 0", paddingLeft: 22 }}>{children}</ListTag>;
|
|
77
197
|
}
|
|
78
198
|
case "listitem":
|
|
79
|
-
|
|
199
|
+
// A nested list sits inside a list item as a block, not as inline text.
|
|
200
|
+
return (
|
|
201
|
+
<li>
|
|
202
|
+
{(node.children ?? []).map((child, i) =>
|
|
203
|
+
child.type === "list" ? <BlockNode key={i} node={child} /> : <InlineNode key={i} node={child} />,
|
|
204
|
+
)}
|
|
205
|
+
</li>
|
|
206
|
+
);
|
|
80
207
|
case "code":
|
|
81
208
|
return (
|
|
82
209
|
<pre
|
|
@@ -93,16 +220,20 @@ function BlockNode({ node }: { node: LexicalNodeJson }) {
|
|
|
93
220
|
</pre>
|
|
94
221
|
);
|
|
95
222
|
default:
|
|
96
|
-
// Unknown block
|
|
223
|
+
// Unknown block: render inline content so nothing is lost.
|
|
97
224
|
return <p style={{ margin: "0 0 0.6em" }}>{children.length ? children : <InlineNode node={node} />}</p>;
|
|
98
225
|
}
|
|
99
226
|
}
|
|
100
227
|
|
|
101
228
|
function InlineNode({ node }: { node: LexicalNodeJson }) {
|
|
102
229
|
const t = useThemeTokens();
|
|
230
|
+
const linkify = useContext(LinkifyContext);
|
|
103
231
|
|
|
104
232
|
if (node.type === "linebreak") return <br />;
|
|
105
233
|
|
|
234
|
+
// An image the editor left inside a paragraph.
|
|
235
|
+
if (node.type === "kb-image" || node.type === "image") return <RichImage node={node} inline />;
|
|
236
|
+
|
|
106
237
|
if (node.type === "mention") {
|
|
107
238
|
const name =
|
|
108
239
|
typeof node.mentionName === "string"
|
|
@@ -110,20 +241,23 @@ function InlineNode({ node }: { node: LexicalNodeJson }) {
|
|
|
110
241
|
: typeof node.text === "string"
|
|
111
242
|
? (node.text as string).replace(/^@/, "")
|
|
112
243
|
: "member";
|
|
113
|
-
return
|
|
114
|
-
<span style={{ color: t.primary, fontWeight: 600 }}>
|
|
115
|
-
@{name}
|
|
116
|
-
</span>
|
|
117
|
-
);
|
|
244
|
+
return <span style={{ color: t.primary, fontWeight: 600 }}>@{name}</span>;
|
|
118
245
|
}
|
|
119
246
|
|
|
120
247
|
if (node.type === "link" || node.type === "autolink") {
|
|
121
|
-
const url =
|
|
248
|
+
const url = safeHref(node.url);
|
|
249
|
+
const inner = (node.children ?? []).map((child, i) => <InlineNode key={i} node={child} />);
|
|
250
|
+
if (!url) return <>{inner}</>;
|
|
251
|
+
// An in-page anchor stays in the page; anything else opens a new tab.
|
|
252
|
+
const external = !url.startsWith("#");
|
|
122
253
|
return (
|
|
123
|
-
<a
|
|
124
|
-
{
|
|
125
|
-
|
|
126
|
-
|
|
254
|
+
<a
|
|
255
|
+
href={url}
|
|
256
|
+
target={external ? "_blank" : undefined}
|
|
257
|
+
rel={external ? "noopener noreferrer" : undefined}
|
|
258
|
+
style={{ color: t.primary, textDecoration: "underline" }}
|
|
259
|
+
>
|
|
260
|
+
{inner}
|
|
127
261
|
</a>
|
|
128
262
|
);
|
|
129
263
|
}
|
|
@@ -132,6 +266,17 @@ function InlineNode({ node }: { node: LexicalNodeJson }) {
|
|
|
132
266
|
// Lexical text format bitmask: 1 bold, 2 italic, 8 underline, 4 strikethrough, 16 code.
|
|
133
267
|
const format = typeof node.format === "number" ? node.format : 0;
|
|
134
268
|
let el: React.ReactNode = node.text as string;
|
|
269
|
+
if (linkify && /https?:\/\//.test(node.text as string)) {
|
|
270
|
+
el = linkifyText(node.text as string).map((segment, i) =>
|
|
271
|
+
segment.kind === "link" ? (
|
|
272
|
+
<a key={i} href={segment.url} target="_blank" rel="noopener noreferrer" style={{ color: t.primary, textDecoration: "underline" }}>
|
|
273
|
+
{segment.text}
|
|
274
|
+
</a>
|
|
275
|
+
) : (
|
|
276
|
+
<span key={i}>{segment.text}</span>
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
135
280
|
if (format & 16)
|
|
136
281
|
el = (
|
|
137
282
|
<code style={{ background: `${t.text}0d`, borderRadius: 4, padding: "0 4px", fontSize: "0.9em" }}>{el}</code>
|