@agent-native/core-corpus 0.217.0 → 0.219.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/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/hubspot/SKILL.md +5 -3
- package/corpus/templates/analytics/CHANGELOG.md +2 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
- package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/assets/CHANGELOG.md +6 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
- package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
- package/corpus/templates/brain/app/routes/sources.tsx +1 -1
- package/corpus/templates/brain/vitest.config.ts +7 -0
- package/corpus/templates/calendar/CHANGELOG.md +6 -0
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
- package/corpus/templates/clips/CHANGELOG.md +7 -20
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
- package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
- package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/CHANGELOG.md +7 -0
- package/corpus/templates/content/app/components/editor/CommentStack.tsx +301 -0
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +770 -179
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +75 -27
- package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
- package/corpus/templates/content/app/components/editor/block-field-comment-threads.ts +15 -0
- package/corpus/templates/content/app/components/editor/block-field-comments.tsx +5 -9
- package/corpus/templates/content/app/components/editor/comment-stacks.ts +143 -0
- package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
- package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
- package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
- package/corpus/templates/content/app/global.css +31 -0
- package/corpus/templates/content/app/i18n-data.ts +8 -0
- package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
- package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-blocks-with-several-comment-threads-fold-into-one-stack-in-t.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-opening-a-page-no-longer-saves-an-edit-again-after-another-o.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +8 -1
- package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
- package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
- package/corpus/templates/content/shared/suggestion-text.ts +20 -5
- package/corpus/templates/design/.generated/bridge/interact-links.generated.ts +8 -8
- package/corpus/templates/design/CHANGELOG.md +8 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +18 -1
- package/corpus/templates/design/app/components/design/bridge/interact-links.bridge.ts +8 -8
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +15 -13
- package/corpus/templates/design/app/i18n/en-US.ts +1 -1
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +18 -64
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +15 -6
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-screen-rendering.tsx +29 -5
- package/corpus/templates/design/app/pages/design-editor/prototype-navigation-transition.ts +423 -0
- package/corpus/templates/design/app/pages/design-editor/view/single-screen-canvas.tsx +35 -9
- package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
- package/corpus/templates/design/changelog/2026-10-20-the-design-full-app-toggle-on-the-home-page-no-longer-hides-.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-prototype-links-can-play-code-authored-screen-transitions.md +6 -0
- package/corpus/templates/design/shared/motion-authored-css-import.ts +938 -7
- package/corpus/templates/design/shared/motion-html-source.ts +1689 -135
- package/corpus/templates/design/shared/motion-prototype-transition.ts +329 -0
- package/corpus/templates/design/shared/motion-react-source.ts +2154 -126
- package/corpus/templates/design/shared/motion-text-runtime-v1.fixture.txt +221 -0
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
- package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
- package/corpus/templates/plan/CHANGELOG.md +6 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
- package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +1 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +7 -2
- package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
- package/package.json +1 -1
- package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
- package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
|
@@ -45,7 +45,11 @@ import {
|
|
|
45
45
|
findReplaceSuggestionSummary,
|
|
46
46
|
suggestionDiffParts,
|
|
47
47
|
} from "@shared/suggestion-diff";
|
|
48
|
-
import
|
|
48
|
+
import {
|
|
49
|
+
suggestionAnchorText,
|
|
50
|
+
suggestionTextForDisplay,
|
|
51
|
+
type SuggestionPresentationContext,
|
|
52
|
+
} from "@shared/suggestion-text";
|
|
49
53
|
|
|
50
54
|
import {
|
|
51
55
|
DropdownMenu,
|
|
@@ -125,6 +129,22 @@ import {
|
|
|
125
129
|
startCommentAiSubmission,
|
|
126
130
|
type CommentAiController,
|
|
127
131
|
} from "./comment-ai";
|
|
132
|
+
import {
|
|
133
|
+
commentBlockKey,
|
|
134
|
+
commentStackFront,
|
|
135
|
+
commentStackKey,
|
|
136
|
+
groupCommentStacks,
|
|
137
|
+
measureCommentBlocks,
|
|
138
|
+
type CommentBlockPlacement,
|
|
139
|
+
type CommentStack,
|
|
140
|
+
} from "./comment-stacks";
|
|
141
|
+
import {
|
|
142
|
+
CommentStackCard,
|
|
143
|
+
CommentStackGuide,
|
|
144
|
+
CommentStackHeader,
|
|
145
|
+
CommentStackSheets,
|
|
146
|
+
CommentStackShowAll,
|
|
147
|
+
} from "./CommentStack";
|
|
128
148
|
import {
|
|
129
149
|
ReviewAddReactionButton,
|
|
130
150
|
ReviewCommentMenu,
|
|
@@ -197,6 +217,20 @@ function isAmbiguousCommentCreateError(error: unknown) {
|
|
|
197
217
|
);
|
|
198
218
|
}
|
|
199
219
|
|
|
220
|
+
/** The session's name for the signed-in person's own entries, if it has one. */
|
|
221
|
+
function ownAuthorName(
|
|
222
|
+
authorEmail: string | null | undefined,
|
|
223
|
+
currentUserEmail?: string,
|
|
224
|
+
currentUserName?: string,
|
|
225
|
+
) {
|
|
226
|
+
const name = currentUserName?.trim();
|
|
227
|
+
return name &&
|
|
228
|
+
authorEmail &&
|
|
229
|
+
authorEmail.trim().toLowerCase() === currentUserEmail?.trim().toLowerCase()
|
|
230
|
+
? name
|
|
231
|
+
: null;
|
|
232
|
+
}
|
|
233
|
+
|
|
200
234
|
function cssEscape(value: string) {
|
|
201
235
|
return globalThis.CSS?.escape
|
|
202
236
|
? globalThis.CSS.escape(value)
|
|
@@ -324,6 +358,26 @@ export function estimateThreadCardHeight(thread: ThreadLayoutIdentity) {
|
|
|
324
358
|
*/
|
|
325
359
|
export const PENDING_COMMENT_LAYOUT_ID = "pending-comment";
|
|
326
360
|
const PENDING_COMMENT_CARD_ESTIMATE = 112;
|
|
361
|
+
/** An open stack's Collapse row, laid out like a card above its threads. */
|
|
362
|
+
const STACK_HEADER_LAYOUT_ID = "comment-stack-header";
|
|
363
|
+
const STACK_HEADER_HEIGHT = 28;
|
|
364
|
+
/** How long the pointer rests on a highlight before its thread comes to the front. */
|
|
365
|
+
const STACK_HOVER_INTENT_MS = 180;
|
|
366
|
+
const NO_BLOCKS: ReadonlyMap<string, CommentBlockPlacement> = new Map();
|
|
367
|
+
const NO_TOUCHES: ReadonlyMap<string, number> = new Map();
|
|
368
|
+
|
|
369
|
+
function sameBlocks(
|
|
370
|
+
left: ReadonlyMap<string, CommentBlockPlacement>,
|
|
371
|
+
right: ReadonlyMap<string, CommentBlockPlacement>,
|
|
372
|
+
) {
|
|
373
|
+
if (left.size !== right.size) return false;
|
|
374
|
+
for (const [threadId, placement] of right) {
|
|
375
|
+
const prior = left.get(threadId);
|
|
376
|
+
if (prior?.key !== placement.key || prior.order !== placement.order)
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
327
381
|
|
|
328
382
|
type CommentLayoutItem<T extends ThreadLayoutIdentity> = {
|
|
329
383
|
thread: T;
|
|
@@ -756,7 +810,11 @@ interface CommentsSidebarOptions {
|
|
|
756
810
|
onActivateSuggestion?: (id: string) => void;
|
|
757
811
|
onSelectedThreadChange?: (id: string | null) => void;
|
|
758
812
|
onHoveredThreadChange?: (id: string | null) => void;
|
|
813
|
+
/** The thread whose highlight the pointer is on. */
|
|
814
|
+
hoveredThreadId?: string | null;
|
|
759
815
|
currentUserEmail?: string;
|
|
816
|
+
/** The signed-in person's display name, for their own suggestions. */
|
|
817
|
+
currentUserName?: string;
|
|
760
818
|
currentUserOrgId?: string;
|
|
761
819
|
canComment?: boolean;
|
|
762
820
|
canResolve?: boolean;
|
|
@@ -840,7 +898,9 @@ export function CommentsSidebar({
|
|
|
840
898
|
onActivateSuggestion,
|
|
841
899
|
onSelectedThreadChange,
|
|
842
900
|
onHoveredThreadChange,
|
|
901
|
+
hoveredThreadId,
|
|
843
902
|
currentUserEmail,
|
|
903
|
+
currentUserName,
|
|
844
904
|
currentUserOrgId,
|
|
845
905
|
canComment = true,
|
|
846
906
|
canResolve = false,
|
|
@@ -1112,36 +1172,254 @@ export function CommentsSidebar({
|
|
|
1112
1172
|
)
|
|
1113
1173
|
setReplyingThreadId(selectedThreadId);
|
|
1114
1174
|
}, [canComment, presentation, selectedThreadId, selectedThreadIsOpen]);
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1175
|
+
|
|
1176
|
+
// In the margin, a block with two or more open threads folds into one
|
|
1177
|
+
// card. Opening it lays its threads out as cards; one stack opens at once.
|
|
1178
|
+
const stacksEnabled =
|
|
1179
|
+
presentation === "inline" &&
|
|
1180
|
+
surface === "rail" &&
|
|
1181
|
+
alignToAnchors &&
|
|
1182
|
+
!visibleThreadId;
|
|
1183
|
+
const [threadBlocks, setThreadBlocks] = useState(NO_BLOCKS);
|
|
1184
|
+
// The block a new comment's text is in, once measured; `key` is null for
|
|
1185
|
+
// text in no block, such as a Property.
|
|
1186
|
+
const [pendingBlock, setPendingBlock] = useState<{
|
|
1187
|
+
id: symbol;
|
|
1188
|
+
key: string | null;
|
|
1189
|
+
} | null>(null);
|
|
1190
|
+
// The open stack's threads, as last laid out. Whichever stack still holds
|
|
1191
|
+
// one of them is open, so it outlasts its block key shifting and the thread
|
|
1192
|
+
// it was opened on being resolved.
|
|
1193
|
+
const [openStack, setOpenStack] = useState<ReadonlySet<string> | null>(null);
|
|
1194
|
+
// When each thread was last touched, so a folded stack shows that one.
|
|
1195
|
+
// Thread ids are unique across tabs, so a touch never crosses tabs.
|
|
1196
|
+
const [stackTouches, setStackTouches] = useState(NO_TOUCHES);
|
|
1197
|
+
const touchCountRef = useRef(0);
|
|
1198
|
+
const touchThread = useCallback((threadId: string) => {
|
|
1199
|
+
touchCountRef.current += 1;
|
|
1200
|
+
const touch = touchCountRef.current;
|
|
1201
|
+
setStackTouches((prev) => new Map(prev).set(threadId, touch));
|
|
1202
|
+
}, []);
|
|
1203
|
+
// A thread just posted is selected without moving the open stack.
|
|
1204
|
+
const postedThreadIdRef = useRef<string | null>(null);
|
|
1205
|
+
const focusStackKeyRef = useRef<string | null>(null);
|
|
1206
|
+
const commentStacks = useMemo(
|
|
1207
|
+
() => (stacksEnabled ? groupCommentStacks(openThreads, threadBlocks) : []),
|
|
1208
|
+
[openThreads, stacksEnabled, threadBlocks],
|
|
1209
|
+
);
|
|
1210
|
+
const stackOfThread = useMemo(() => {
|
|
1211
|
+
const stacks = new Map<string, CommentStack<CommentThread>>();
|
|
1212
|
+
for (const stack of commentStacks) {
|
|
1213
|
+
for (const member of stack.threads) stacks.set(member.threadId, stack);
|
|
1124
1214
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1215
|
+
return stacks;
|
|
1216
|
+
}, [commentStacks]);
|
|
1217
|
+
const stackOfThreadRef = useRef(stackOfThread);
|
|
1218
|
+
stackOfThreadRef.current = stackOfThread;
|
|
1219
|
+
const pendingStackKey =
|
|
1220
|
+
displayedPendingComment && pendingBlock?.id === displayedPendingComment.id
|
|
1221
|
+
? pendingBlock.key
|
|
1222
|
+
: undefined;
|
|
1223
|
+
const openedStack = openStack
|
|
1224
|
+
? (commentStacks.find((stack) =>
|
|
1225
|
+
stack.threads.some((member) => openStack.has(member.threadId)),
|
|
1226
|
+
) ?? null)
|
|
1227
|
+
: null;
|
|
1228
|
+
// A new comment outside the open stack folds it, as does leaving it with
|
|
1229
|
+
// nothing selected.
|
|
1230
|
+
const pendingFoldsOpenStack =
|
|
1231
|
+
pendingStackKey !== undefined &&
|
|
1232
|
+
pendingStackKey !== (openedStack?.key ?? null);
|
|
1233
|
+
const expandedStack =
|
|
1234
|
+
pendingFoldsOpenStack || (!selectedThreadId && !pendingComment)
|
|
1235
|
+
? null
|
|
1236
|
+
: openedStack;
|
|
1237
|
+
// A new comment on a folded stack opens on top of it.
|
|
1238
|
+
const composingStack =
|
|
1239
|
+
pendingStackKey == null || pendingStackKey === expandedStack?.key
|
|
1240
|
+
? null
|
|
1241
|
+
: (commentStacks.find((stack) => stack.key === pendingStackKey) ?? null);
|
|
1242
|
+
|
|
1243
|
+
const openStackOn = useCallback((threadId: string) => {
|
|
1244
|
+
const stack = stackOfThreadRef.current.get(threadId);
|
|
1245
|
+
setOpenStack(
|
|
1246
|
+
new Set(
|
|
1247
|
+
stack ? stack.threads.map((member) => member.threadId) : [threadId],
|
|
1248
|
+
),
|
|
1249
|
+
);
|
|
1250
|
+
}, []);
|
|
1251
|
+
const previousSelectionRef = useRef<string | null>(null);
|
|
1252
|
+
useEffect(() => {
|
|
1253
|
+
const previous = previousSelectionRef.current;
|
|
1254
|
+
previousSelectionRef.current = selectedThreadId ?? null;
|
|
1255
|
+
if (!stacksEnabled || !selectedThreadId || selectedThreadId === previous)
|
|
1256
|
+
return;
|
|
1257
|
+
touchThread(selectedThreadId);
|
|
1258
|
+
const posted = postedThreadIdRef.current === selectedThreadId;
|
|
1259
|
+
postedThreadIdRef.current = null;
|
|
1260
|
+
if (!posted) openStackOn(selectedThreadId);
|
|
1261
|
+
}, [openStackOn, selectedThreadId, stacksEnabled, touchThread]);
|
|
1262
|
+
useEffect(() => {
|
|
1263
|
+
if (!openStack) return;
|
|
1264
|
+
if ((!selectedThreadId && !pendingComment) || pendingFoldsOpenStack) {
|
|
1265
|
+
setOpenStack(null);
|
|
1266
|
+
} else if (openedStack) {
|
|
1267
|
+
const members = openedStack.threads;
|
|
1268
|
+
if (
|
|
1269
|
+
members.length !== openStack.size ||
|
|
1270
|
+
members.some((member) => !openStack.has(member.threadId))
|
|
1271
|
+
)
|
|
1272
|
+
setOpenStack(new Set(members.map((member) => member.threadId)));
|
|
1273
|
+
} else if (
|
|
1274
|
+
// A thread selected before the margin measured its block may still
|
|
1275
|
+
// turn out to stack. Otherwise none of them does, and keeping them
|
|
1276
|
+
// would open a stack their block forms later by itself.
|
|
1277
|
+
!openThreads.some(
|
|
1278
|
+
(thread) =>
|
|
1279
|
+
!thread.resolved &&
|
|
1280
|
+
openStack.has(thread.threadId) &&
|
|
1281
|
+
!threadBlocks.has(thread.threadId),
|
|
1282
|
+
)
|
|
1283
|
+
) {
|
|
1284
|
+
setOpenStack(null);
|
|
1132
1285
|
}
|
|
1286
|
+
}, [
|
|
1287
|
+
openStack,
|
|
1288
|
+
openThreads,
|
|
1289
|
+
openedStack,
|
|
1290
|
+
pendingComment,
|
|
1291
|
+
pendingFoldsOpenStack,
|
|
1292
|
+
selectedThreadId,
|
|
1293
|
+
threadBlocks,
|
|
1294
|
+
]);
|
|
1295
|
+
// A highlight click opens its stack and touches its thread, even one already
|
|
1296
|
+
// selected, whose selection doesn't change.
|
|
1297
|
+
useEffect(() => {
|
|
1298
|
+
const container = scrollContainerRef?.current;
|
|
1299
|
+
if (!stacksEnabled || !container) return;
|
|
1300
|
+
const openFromHighlight = (event: MouseEvent) => {
|
|
1301
|
+
const threadId = (event.target as Element | null)
|
|
1302
|
+
?.closest?.("[data-comment-thread]")
|
|
1303
|
+
?.getAttribute("data-comment-thread");
|
|
1304
|
+
if (!threadId || !stackOfThreadRef.current.has(threadId)) return;
|
|
1305
|
+
touchThread(threadId);
|
|
1306
|
+
openStackOn(threadId);
|
|
1307
|
+
};
|
|
1308
|
+
container.addEventListener("click", openFromHighlight);
|
|
1309
|
+
return () => container.removeEventListener("click", openFromHighlight);
|
|
1310
|
+
}, [openStackOn, scrollContainerRef, stacksEnabled, touchThread]);
|
|
1311
|
+
// Resting on a highlight brings its thread to the front of a folded stack;
|
|
1312
|
+
// passing over one on the way to the margin doesn't.
|
|
1313
|
+
useEffect(() => {
|
|
1314
|
+
if (!hoveredThreadId) return;
|
|
1315
|
+
const stack = stackOfThread.get(hoveredThreadId);
|
|
1316
|
+
if (!stack || stack === expandedStack) return;
|
|
1317
|
+
// Touching the stack's last-touched thread again would only rerun this.
|
|
1318
|
+
const touch = stackTouches.get(hoveredThreadId);
|
|
1319
|
+
if (
|
|
1320
|
+
touch !== undefined &&
|
|
1321
|
+
stack.threads.every(
|
|
1322
|
+
(member) => (stackTouches.get(member.threadId) ?? 0) <= touch,
|
|
1323
|
+
)
|
|
1324
|
+
)
|
|
1325
|
+
return;
|
|
1326
|
+
const timer = setTimeout(
|
|
1327
|
+
() => touchThread(hoveredThreadId),
|
|
1328
|
+
STACK_HOVER_INTENT_MS,
|
|
1329
|
+
);
|
|
1330
|
+
return () => clearTimeout(timer);
|
|
1331
|
+
}, [
|
|
1332
|
+
expandedStack,
|
|
1333
|
+
hoveredThreadId,
|
|
1334
|
+
stackOfThread,
|
|
1335
|
+
stackTouches,
|
|
1336
|
+
touchThread,
|
|
1337
|
+
]);
|
|
1338
|
+
const expandedStackKey = expandedStack?.key ?? null;
|
|
1339
|
+
const selectedInExpandedStack =
|
|
1340
|
+
!!selectedThreadId &&
|
|
1341
|
+
!!expandedStack?.threads.some(
|
|
1342
|
+
(member) => member.threadId === selectedThreadId,
|
|
1343
|
+
);
|
|
1344
|
+
useLayoutEffect(() => {
|
|
1345
|
+
if (!selectedInExpandedStack || !selectedThreadId) return;
|
|
1346
|
+
const card = sidebarRef.current?.querySelector<HTMLElement>(
|
|
1347
|
+
`[data-thread-card="${cssEscape(selectedThreadId)}"]`,
|
|
1348
|
+
);
|
|
1349
|
+
if (!card) return;
|
|
1350
|
+
// Opening from the folded card removes the button that had focus.
|
|
1351
|
+
const active = document.activeElement;
|
|
1352
|
+
if (!active || active === document.body)
|
|
1353
|
+
card.focus({ preventScroll: true });
|
|
1354
|
+
const timer = setTimeout(
|
|
1355
|
+
() => card.scrollIntoView?.({ block: "nearest", behavior: "smooth" }),
|
|
1356
|
+
300,
|
|
1357
|
+
);
|
|
1358
|
+
return () => clearTimeout(timer);
|
|
1359
|
+
}, [expandedStackKey, selectedInExpandedStack, selectedThreadId]);
|
|
1360
|
+
const pendingInExpandedStack =
|
|
1361
|
+
!!expandedStack && pendingStackKey === expandedStack.key;
|
|
1362
|
+
useLayoutEffect(() => {
|
|
1363
|
+
if (!pendingInExpandedStack) return;
|
|
1364
|
+
// Show all, under a new comment, removes the button that had focus.
|
|
1365
|
+
const active = document.activeElement;
|
|
1366
|
+
if (active && active !== document.body) return;
|
|
1367
|
+
sidebarRef.current
|
|
1368
|
+
?.querySelector<HTMLElement>(
|
|
1369
|
+
'[data-comment-pending] [contenteditable="true"]',
|
|
1370
|
+
)
|
|
1371
|
+
?.focus({ preventScroll: true });
|
|
1372
|
+
}, [pendingInExpandedStack]);
|
|
1373
|
+
useLayoutEffect(() => {
|
|
1374
|
+
const key = focusStackKeyRef.current;
|
|
1375
|
+
if (key === null) return;
|
|
1376
|
+
const stack = [
|
|
1377
|
+
...(sidebarRef.current?.querySelectorAll<HTMLElement>(
|
|
1378
|
+
"[data-comment-stack]",
|
|
1379
|
+
) ?? []),
|
|
1380
|
+
].find((element) => element.dataset.commentStack === key);
|
|
1381
|
+
const target = stack?.querySelector<HTMLElement>(
|
|
1382
|
+
"[data-comment-stack-open], [data-comment-stack-show-all]",
|
|
1383
|
+
);
|
|
1384
|
+
if (!target) return;
|
|
1385
|
+
focusStackKeyRef.current = null;
|
|
1386
|
+
target.focus({ preventScroll: true });
|
|
1387
|
+
});
|
|
1388
|
+
const collapseStack = (stack: CommentStack<CommentThread>) => {
|
|
1389
|
+
focusStackKeyRef.current = stack.key;
|
|
1390
|
+
setOpenStack(null);
|
|
1391
|
+
setReplyingThreadId(null);
|
|
1392
|
+
onSelectedThreadChange?.(null);
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
const historyAuthors = useMemo(() => {
|
|
1396
|
+
const authors = new Map<string, string>();
|
|
1397
|
+
// A name from any record outranks the email's local part from another.
|
|
1398
|
+
const addAuthor = (email: string | null, name?: string | null) => {
|
|
1399
|
+
if (!email) return;
|
|
1400
|
+
const known =
|
|
1401
|
+
name?.trim() || ownAuthorName(email, currentUserEmail, currentUserName);
|
|
1402
|
+
if (known) authors.set(email, known);
|
|
1403
|
+
else if (!authors.has(email)) authors.set(email, email.split("@")[0]);
|
|
1404
|
+
};
|
|
1405
|
+
for (const suggestion of suggestions) addAuthor(suggestion.authorEmail);
|
|
1406
|
+
for (const suggestion of draftSuggestions)
|
|
1407
|
+
addAuthor(suggestion.authorEmail);
|
|
1133
1408
|
for (const thread of threads) {
|
|
1134
1409
|
for (const comment of thread.comments) {
|
|
1135
|
-
|
|
1136
|
-
comment.author_email,
|
|
1137
|
-
comment.author_name ?? comment.author_email.split("@")[0],
|
|
1138
|
-
);
|
|
1410
|
+
addAuthor(comment.author_email, comment.author_name);
|
|
1139
1411
|
}
|
|
1140
1412
|
}
|
|
1141
1413
|
return [...authors.entries()].sort((left, right) =>
|
|
1142
1414
|
left[1].localeCompare(right[1]),
|
|
1143
1415
|
);
|
|
1144
|
-
}, [
|
|
1416
|
+
}, [
|
|
1417
|
+
currentUserEmail,
|
|
1418
|
+
currentUserName,
|
|
1419
|
+
draftSuggestions,
|
|
1420
|
+
suggestions,
|
|
1421
|
+
threads,
|
|
1422
|
+
]);
|
|
1145
1423
|
// Suggestions edit the page body, so they sit in its tab.
|
|
1146
1424
|
const historyBodyTabShown =
|
|
1147
1425
|
activeHistoryTab === null || activeHistoryTab === commentTabKey(null);
|
|
@@ -1320,6 +1598,7 @@ export function CommentsSidebar({
|
|
|
1320
1598
|
pendingDraft.beginSubmission(clientOperationId);
|
|
1321
1599
|
onPendingChange(id, () => ({ submitting: true }));
|
|
1322
1600
|
setPendingHandoff({ id, operationId: clientOperationId });
|
|
1601
|
+
touchThread(`optimistic-${clientOperationId}`);
|
|
1323
1602
|
try {
|
|
1324
1603
|
const result = await createComment.mutateAsync({
|
|
1325
1604
|
clientOperationId,
|
|
@@ -1343,7 +1622,11 @@ export function CommentsSidebar({
|
|
|
1343
1622
|
});
|
|
1344
1623
|
pendingDraft.finishSubmission(clientOperationId);
|
|
1345
1624
|
setPendingHandoff(null);
|
|
1625
|
+
// The new thread is its stack's front. Selecting it leaves its stack
|
|
1626
|
+
// as it was, open or folded.
|
|
1627
|
+
touchThread(result.threadId);
|
|
1346
1628
|
if (pendingCommentRef.current?.id === id) {
|
|
1629
|
+
postedThreadIdRef.current = result.threadId;
|
|
1347
1630
|
onPendingDone(id, result.threadId);
|
|
1348
1631
|
}
|
|
1349
1632
|
return { id: result.id, threadId: result.threadId };
|
|
@@ -1465,6 +1748,22 @@ export function CommentsSidebar({
|
|
|
1465
1748
|
null,
|
|
1466
1749
|
);
|
|
1467
1750
|
const pendingCardRef = useRef<HTMLDivElement>(null);
|
|
1751
|
+
// An inline group remounts under its next card once its leading edit is
|
|
1752
|
+
// decided, so whether it is open has to outlive the group.
|
|
1753
|
+
const [expandedProposalIds, setExpandedProposalIds] = useState<
|
|
1754
|
+
ReadonlySet<string>
|
|
1755
|
+
>(() => new Set());
|
|
1756
|
+
const setProposalExpanded = useCallback(
|
|
1757
|
+
(proposalId: string, expanded: boolean) =>
|
|
1758
|
+
setExpandedProposalIds((current) => {
|
|
1759
|
+
if (current.has(proposalId) === expanded) return current;
|
|
1760
|
+
const next = new Set(current);
|
|
1761
|
+
if (expanded) next.add(proposalId);
|
|
1762
|
+
else next.delete(proposalId);
|
|
1763
|
+
return next;
|
|
1764
|
+
}),
|
|
1765
|
+
[],
|
|
1766
|
+
);
|
|
1468
1767
|
const inlineProposalLeaders = useMemo(() => {
|
|
1469
1768
|
const leaders = new Map<string, string>();
|
|
1470
1769
|
for (const proposalId of inlineProposalMembers.keys()) {
|
|
@@ -1487,18 +1786,35 @@ export function CommentsSidebar({
|
|
|
1487
1786
|
proposalMemberCounts,
|
|
1488
1787
|
threadPositions,
|
|
1489
1788
|
]);
|
|
1789
|
+
// A folded stack is laid out as its first thread; a new comment opened on
|
|
1790
|
+
// top of it takes its place.
|
|
1791
|
+
const stackHiddenIds = useMemo(() => {
|
|
1792
|
+
const hidden = new Set<string>();
|
|
1793
|
+
for (const stack of commentStacks) {
|
|
1794
|
+
if (stack === expandedStack) continue;
|
|
1795
|
+
const folded =
|
|
1796
|
+
stack === composingStack ? stack.threads : stack.threads.slice(1);
|
|
1797
|
+
for (const member of folded) hidden.add(member.threadId);
|
|
1798
|
+
}
|
|
1799
|
+
return hidden;
|
|
1800
|
+
}, [commentStacks, composingStack, expandedStack]);
|
|
1490
1801
|
const layoutThreads = useMemo(
|
|
1491
1802
|
() =>
|
|
1492
|
-
inlineThreads.filter(
|
|
1493
|
-
|
|
1494
|
-
!("
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1803
|
+
inlineThreads.filter((thread) =>
|
|
1804
|
+
"suggestion" in thread
|
|
1805
|
+
? !("proposalId" in thread.suggestion) ||
|
|
1806
|
+
!thread.suggestion.proposalId ||
|
|
1807
|
+
(proposalMemberCounts.get(thread.suggestion.proposalId) ?? 0) < 2 ||
|
|
1808
|
+
inlineProposalLeaders.get(thread.suggestion.proposalId) ===
|
|
1809
|
+
thread.threadId
|
|
1810
|
+
: !stackHiddenIds.has(thread.threadId),
|
|
1500
1811
|
),
|
|
1501
|
-
[
|
|
1812
|
+
[
|
|
1813
|
+
inlineThreads,
|
|
1814
|
+
inlineProposalLeaders,
|
|
1815
|
+
proposalMemberCounts,
|
|
1816
|
+
stackHiddenIds,
|
|
1817
|
+
],
|
|
1502
1818
|
);
|
|
1503
1819
|
const openThreadKey = inlineThreads
|
|
1504
1820
|
.map(
|
|
@@ -1520,9 +1836,36 @@ export function CommentsSidebar({
|
|
|
1520
1836
|
);
|
|
1521
1837
|
|
|
1522
1838
|
const hasPendingComment = !!displayedPendingComment;
|
|
1839
|
+
const pendingCommentId = displayedPendingComment?.id;
|
|
1840
|
+
const pendingFieldId = displayedPendingComment?.fieldId ?? null;
|
|
1841
|
+
const pendingOnProperty = !!displayedPendingComment?.property;
|
|
1523
1842
|
const recomputeOffsets = useCallback(() => {
|
|
1524
1843
|
const container = scrollContainerRef?.current ?? null;
|
|
1525
1844
|
setThreadPositionsMeasured(!!container);
|
|
1845
|
+
const blocks =
|
|
1846
|
+
stacksEnabled && container
|
|
1847
|
+
? measureCommentBlocks(container, openThreads)
|
|
1848
|
+
: NO_BLOCKS;
|
|
1849
|
+
setThreadBlocks((prev) => (sameBlocks(prev, blocks) ? prev : blocks));
|
|
1850
|
+
if (stacksEnabled && container && pendingCommentId) {
|
|
1851
|
+
const highlight = pendingOnProperty
|
|
1852
|
+
? null
|
|
1853
|
+
: container.querySelector(".comment-highlight--pending");
|
|
1854
|
+
const block =
|
|
1855
|
+
highlight && !inHiddenBlockFieldPanel(highlight)
|
|
1856
|
+
? commentBlockKey(highlight)
|
|
1857
|
+
: null;
|
|
1858
|
+
// Text not drawn yet isn't known to be outside the open stack.
|
|
1859
|
+
if (pendingOnProperty || highlight) {
|
|
1860
|
+
const key =
|
|
1861
|
+
block === null ? null : commentStackKey(pendingFieldId, block);
|
|
1862
|
+
setPendingBlock((prev) =>
|
|
1863
|
+
prev?.id === pendingCommentId && prev.key === key
|
|
1864
|
+
? prev
|
|
1865
|
+
: { id: pendingCommentId, key },
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1526
1869
|
const layoutContainer = alignToAnchors ? sidebarRef.current : null;
|
|
1527
1870
|
const nextPendingPosition =
|
|
1528
1871
|
hasPendingComment && alignToAnchors
|
|
@@ -1577,7 +1920,17 @@ export function CommentsSidebar({
|
|
|
1577
1920
|
}
|
|
1578
1921
|
return positions;
|
|
1579
1922
|
});
|
|
1580
|
-
}, [
|
|
1923
|
+
}, [
|
|
1924
|
+
alignToAnchors,
|
|
1925
|
+
hasPendingComment,
|
|
1926
|
+
inlineThreads,
|
|
1927
|
+
openThreads,
|
|
1928
|
+
pendingCommentId,
|
|
1929
|
+
pendingFieldId,
|
|
1930
|
+
pendingOnProperty,
|
|
1931
|
+
scrollContainerRef,
|
|
1932
|
+
stacksEnabled,
|
|
1933
|
+
]);
|
|
1581
1934
|
|
|
1582
1935
|
// Measured before paint, so a card under a new id never paints unplaced.
|
|
1583
1936
|
useLayoutEffect(() => {
|
|
@@ -1601,14 +1954,18 @@ export function CommentsSidebar({
|
|
|
1601
1954
|
subtree: true,
|
|
1602
1955
|
characterData: true,
|
|
1603
1956
|
attributes: true,
|
|
1604
|
-
attributeFilter: ["hidden"],
|
|
1957
|
+
attributeFilter: ["hidden", "data-comment-thread"],
|
|
1605
1958
|
});
|
|
1606
1959
|
}
|
|
1607
1960
|
} else {
|
|
1961
|
+
// A posted comment's highlight can reuse the draft's span, gaining only
|
|
1962
|
+
// its thread id, and a stack needs that id to place the thread.
|
|
1608
1963
|
observer.observe(primaryBlockFieldEditor(container) ?? container, {
|
|
1609
1964
|
childList: true,
|
|
1610
1965
|
subtree: true,
|
|
1611
1966
|
characterData: true,
|
|
1967
|
+
attributes: true,
|
|
1968
|
+
attributeFilter: ["data-comment-thread"],
|
|
1612
1969
|
});
|
|
1613
1970
|
}
|
|
1614
1971
|
const resizeObserver =
|
|
@@ -1627,7 +1984,6 @@ export function CommentsSidebar({
|
|
|
1627
1984
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1628
1985
|
}, [openThreadKey, hasPendingComment, recomputeOffsets]);
|
|
1629
1986
|
|
|
1630
|
-
const pendingCommentId = displayedPendingComment?.id;
|
|
1631
1987
|
useLayoutEffect(() => {
|
|
1632
1988
|
const element = pendingCardRef.current;
|
|
1633
1989
|
if (!element) {
|
|
@@ -1678,6 +2034,10 @@ export function CommentsSidebar({
|
|
|
1678
2034
|
const activeSavedSuggestion = inlineSuggestions.find(
|
|
1679
2035
|
(suggestion) => suggestion.id === activeSuggestionId,
|
|
1680
2036
|
);
|
|
2037
|
+
// A thread in a folded stack is laid out as the stack's card.
|
|
2038
|
+
const selectedStack = selectedThreadId
|
|
2039
|
+
? stackOfThread.get(selectedThreadId)
|
|
2040
|
+
: undefined;
|
|
1681
2041
|
const selectedLayoutThreadId =
|
|
1682
2042
|
(activeSavedSuggestion?.proposalId
|
|
1683
2043
|
? inlineProposalLeaders.get(activeSavedSuggestion.proposalId)
|
|
@@ -1685,7 +2045,9 @@ export function CommentsSidebar({
|
|
|
1685
2045
|
inlineDraftSuggestions.find(
|
|
1686
2046
|
(suggestion) => suggestion.id === activeSuggestionId,
|
|
1687
2047
|
)?.threadId ??
|
|
1688
|
-
|
|
2048
|
+
(selectedStack && selectedStack !== expandedStack
|
|
2049
|
+
? selectedStack.threads[0].threadId
|
|
2050
|
+
: selectedThreadId);
|
|
1689
2051
|
const restingItems = layoutCommentThreads(
|
|
1690
2052
|
layoutThreads,
|
|
1691
2053
|
threadPositions,
|
|
@@ -1702,32 +2064,105 @@ export function CommentsSidebar({
|
|
|
1702
2064
|
: null;
|
|
1703
2065
|
const pendingAnchorTop =
|
|
1704
2066
|
pendingPosition?.layoutTop ?? displayedPendingComment?.offsetTop ?? 0;
|
|
1705
|
-
const
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
2067
|
+
const pendingAnchor: CommentThreadPosition = {
|
|
2068
|
+
documentTop: pendingPosition?.documentTop ?? pendingAnchorTop,
|
|
2069
|
+
layoutTop: pendingAnchorTop,
|
|
2070
|
+
};
|
|
2071
|
+
// An open stack's Collapse row is laid out like a card above its first one.
|
|
2072
|
+
const stackHeaderLayout: ThreadLayoutIdentity | null = expandedStack
|
|
2073
|
+
? { threadId: STACK_HEADER_LAYOUT_ID, comments: [] }
|
|
2074
|
+
: null;
|
|
2075
|
+
const firstStackAnchor = expandedStack
|
|
2076
|
+
? [
|
|
2077
|
+
...expandedStack.threads.map((member) =>
|
|
2078
|
+
threadPositions.get(member.threadId),
|
|
1715
2079
|
),
|
|
1716
|
-
|
|
2080
|
+
pendingInExpandedStack ? pendingAnchor : undefined,
|
|
2081
|
+
].reduce<CommentThreadPosition | null>(
|
|
2082
|
+
(first, position) =>
|
|
2083
|
+
position && (!first || position.documentTop < first.documentTop)
|
|
2084
|
+
? position
|
|
2085
|
+
: first,
|
|
2086
|
+
null,
|
|
1717
2087
|
)
|
|
1718
|
-
:
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
2088
|
+
: null;
|
|
2089
|
+
const layoutPositions = new Map(threadPositions);
|
|
2090
|
+
const layoutHeights = new Map(threadCardHeights);
|
|
2091
|
+
if (pendingLayout) {
|
|
2092
|
+
layoutPositions.set(PENDING_COMMENT_LAYOUT_ID, pendingAnchor);
|
|
2093
|
+
layoutHeights.set(
|
|
2094
|
+
PENDING_COMMENT_LAYOUT_ID,
|
|
2095
|
+
pendingCardHeight ?? PENDING_COMMENT_CARD_ESTIMATE,
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
if (stackHeaderLayout) {
|
|
2099
|
+
if (firstStackAnchor) {
|
|
2100
|
+
layoutPositions.set(STACK_HEADER_LAYOUT_ID, {
|
|
2101
|
+
documentTop: firstStackAnchor.documentTop - 0.5,
|
|
2102
|
+
layoutTop:
|
|
2103
|
+
firstStackAnchor.layoutTop === null
|
|
2104
|
+
? null
|
|
2105
|
+
: firstStackAnchor.layoutTop - STACK_HEADER_HEIGHT - 12,
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
layoutHeights.set(STACK_HEADER_LAYOUT_ID, STACK_HEADER_HEIGHT);
|
|
2109
|
+
}
|
|
2110
|
+
const anchoredItems = layoutCommentThreads<
|
|
2111
|
+
LayoutThread | ThreadLayoutIdentity
|
|
2112
|
+
>(
|
|
2113
|
+
[
|
|
2114
|
+
...layoutThreads,
|
|
2115
|
+
...(pendingLayout ? [pendingLayout] : []),
|
|
2116
|
+
...(stackHeaderLayout ? [stackHeaderLayout] : []),
|
|
2117
|
+
],
|
|
2118
|
+
layoutPositions,
|
|
2119
|
+
layoutHeights,
|
|
2120
|
+
pendingLayout ? PENDING_COMMENT_LAYOUT_ID : selectedLayoutThreadId,
|
|
2121
|
+
);
|
|
2122
|
+
// Keeping the selected card level with its highlight pushes the cards above
|
|
2123
|
+
// it upward. Near the top of a page that would put an open stack's Collapse
|
|
2124
|
+
// row and first cards above the margin, where nothing can scroll to them, so
|
|
2125
|
+
// the stack starts at the top instead.
|
|
2126
|
+
const stackOverflow = Math.max(
|
|
2127
|
+
0,
|
|
2128
|
+
-(
|
|
2129
|
+
anchoredItems.find((item) => item.thread === stackHeaderLayout)?.top ?? 0
|
|
2130
|
+
),
|
|
2131
|
+
);
|
|
2132
|
+
const selectedItems = stackOverflow
|
|
2133
|
+
? anchoredItems.map((item) => ({ ...item, top: item.top + stackOverflow }))
|
|
2134
|
+
: anchoredItems;
|
|
1724
2135
|
const pendingTop =
|
|
1725
2136
|
selectedItems.find((item) => item.thread === pendingLayout)?.top ??
|
|
1726
2137
|
pendingAnchorTop;
|
|
2138
|
+
const stackHeaderTop = selectedItems.find(
|
|
2139
|
+
(item) => item.thread === stackHeaderLayout,
|
|
2140
|
+
)?.top;
|
|
1727
2141
|
const items = selectedItems.filter(
|
|
1728
2142
|
(item): item is CommentLayoutItem<LayoutThread> =>
|
|
1729
|
-
item.thread !== pendingLayout,
|
|
2143
|
+
item.thread !== pendingLayout && item.thread !== stackHeaderLayout,
|
|
1730
2144
|
);
|
|
2145
|
+
// The guide beside an open stack spans its cards, and a new comment in it.
|
|
2146
|
+
const stackSpans = expandedStack
|
|
2147
|
+
? selectedItems
|
|
2148
|
+
.filter((item) =>
|
|
2149
|
+
item.thread === pendingLayout
|
|
2150
|
+
? pendingInExpandedStack
|
|
2151
|
+
: expandedStack.threads.some(
|
|
2152
|
+
(member) => member.threadId === item.thread.threadId,
|
|
2153
|
+
),
|
|
2154
|
+
)
|
|
2155
|
+
.map((item) => ({
|
|
2156
|
+
top: item.top,
|
|
2157
|
+
bottom:
|
|
2158
|
+
item.top +
|
|
2159
|
+
(layoutHeights.get(item.thread.threadId) ??
|
|
2160
|
+
estimateThreadCardHeight(item.thread)),
|
|
2161
|
+
}))
|
|
2162
|
+
: [];
|
|
2163
|
+
const stackGuideTop = Math.min(...stackSpans.map((span) => span.top)) + 16;
|
|
2164
|
+
const stackGuideHeight =
|
|
2165
|
+
Math.max(...stackSpans.map((span) => span.bottom)) - 16 - stackGuideTop;
|
|
1731
2166
|
|
|
1732
2167
|
const changeResolution = async (thread: CommentThread, resolved: boolean) => {
|
|
1733
2168
|
if (
|
|
@@ -1773,10 +2208,39 @@ export function CommentsSidebar({
|
|
|
1773
2208
|
);
|
|
1774
2209
|
};
|
|
1775
2210
|
|
|
2211
|
+
const renderFoldedStack = (
|
|
2212
|
+
stack: CommentStack<CommentThread>,
|
|
2213
|
+
layoutId: string,
|
|
2214
|
+
) => {
|
|
2215
|
+
const front = commentStackFront(stack.threads, stackTouches);
|
|
2216
|
+
return (
|
|
2217
|
+
<CommentStackCard
|
|
2218
|
+
stackKey={stack.key}
|
|
2219
|
+
heightId={layoutId}
|
|
2220
|
+
threads={stack.threads}
|
|
2221
|
+
front={front}
|
|
2222
|
+
isActive={stack.threads.some(
|
|
2223
|
+
(member) => member.threadId === activeThreadId,
|
|
2224
|
+
)}
|
|
2225
|
+
onOpen={() => {
|
|
2226
|
+
openStackOn(front.threadId);
|
|
2227
|
+
onActivateThread?.(front.threadId);
|
|
2228
|
+
if (canComment) setReplyingThreadId(front.threadId);
|
|
2229
|
+
}}
|
|
2230
|
+
onHoverChange={(hovered) =>
|
|
2231
|
+
onHoveredThreadChange?.(hovered ? front.threadId : null)
|
|
2232
|
+
}
|
|
2233
|
+
onHeightChange={handleThreadCardHeightChange}
|
|
2234
|
+
t={t}
|
|
2235
|
+
/>
|
|
2236
|
+
);
|
|
2237
|
+
};
|
|
2238
|
+
|
|
1776
2239
|
const renderCommentThread = (
|
|
1777
2240
|
thread: CommentThread,
|
|
1778
2241
|
marginTop = 0,
|
|
1779
2242
|
isActive = false,
|
|
2243
|
+
inStack = false,
|
|
1780
2244
|
) => {
|
|
1781
2245
|
const aiRequest = commentAi
|
|
1782
2246
|
? latestCommentAiRequest(commentAi.requests, thread.threadId)
|
|
@@ -1984,6 +2448,7 @@ export function CommentsSidebar({
|
|
|
1984
2448
|
threadActions={renderThreadActions?.(thread)}
|
|
1985
2449
|
currentUserEmail={currentUserEmail}
|
|
1986
2450
|
quote={thread.quotedText}
|
|
2451
|
+
showQuote={inStack}
|
|
1987
2452
|
propertyTarget={
|
|
1988
2453
|
thread.property
|
|
1989
2454
|
? propertyThreadTarget(thread.property, documentProperties)
|
|
@@ -2097,6 +2562,7 @@ export function CommentsSidebar({
|
|
|
2097
2562
|
surface={surface}
|
|
2098
2563
|
onClose={onClose}
|
|
2099
2564
|
currentUserEmail={currentUserEmail}
|
|
2565
|
+
currentUserName={currentUserName}
|
|
2100
2566
|
compact={compact}
|
|
2101
2567
|
replyDrafts={replyDrafts}
|
|
2102
2568
|
key={suggestion.id}
|
|
@@ -2144,6 +2610,8 @@ export function CommentsSidebar({
|
|
|
2144
2610
|
(member) =>
|
|
2145
2611
|
member.id === activeSuggestionId || member.id === focusSuggestionId,
|
|
2146
2612
|
)}
|
|
2613
|
+
expanded={expandedProposalIds.has(proposalId)}
|
|
2614
|
+
onExpandedChange={(expanded) => setProposalExpanded(proposalId, expanded)}
|
|
2147
2615
|
deciding={members.some(suggestionBusy)}
|
|
2148
2616
|
canDecide={canDecideSuggestions && !!onDecideSuggestionProposal}
|
|
2149
2617
|
onDecide={(decision) =>
|
|
@@ -2176,6 +2644,7 @@ export function CommentsSidebar({
|
|
|
2176
2644
|
surface={surface}
|
|
2177
2645
|
onClose={onClose}
|
|
2178
2646
|
currentUserEmail={currentUserEmail}
|
|
2647
|
+
currentUserName={currentUserName}
|
|
2179
2648
|
key={suggestion.id}
|
|
2180
2649
|
marginTop={marginTop}
|
|
2181
2650
|
onHeightChange={handleThreadCardHeightChange}
|
|
@@ -2443,6 +2912,18 @@ export function CommentsSidebar({
|
|
|
2443
2912
|
ref={sidebarRef}
|
|
2444
2913
|
className="relative flow-root w-full min-w-0 shrink-0 pb-16"
|
|
2445
2914
|
data-comments-sidebar
|
|
2915
|
+
onClick={(event) => {
|
|
2916
|
+
// The margin's empty space is outside every card, so it folds.
|
|
2917
|
+
const target = event.target as HTMLElement;
|
|
2918
|
+
if (
|
|
2919
|
+
expandedStack &&
|
|
2920
|
+
(target === event.currentTarget ||
|
|
2921
|
+
target.hasAttribute("data-comment-layout-thread") ||
|
|
2922
|
+
target.dataset.commentStackState === "open" ||
|
|
2923
|
+
target.hasAttribute("data-comment-stack-guide"))
|
|
2924
|
+
)
|
|
2925
|
+
collapseStack(expandedStack);
|
|
2926
|
+
}}
|
|
2446
2927
|
>
|
|
2447
2928
|
{isLoading ? (
|
|
2448
2929
|
<div className="space-y-3 px-2 pt-3" aria-hidden="true">
|
|
@@ -2454,108 +2935,143 @@ export function CommentsSidebar({
|
|
|
2454
2935
|
))}
|
|
2455
2936
|
</div>
|
|
2456
2937
|
) : null}
|
|
2938
|
+
{expandedStack && stackHeaderTop !== undefined ? (
|
|
2939
|
+
<CommentStackHeader
|
|
2940
|
+
stackKey={expandedStack.key}
|
|
2941
|
+
top={stackHeaderTop}
|
|
2942
|
+
onCollapse={() => collapseStack(expandedStack)}
|
|
2943
|
+
t={t}
|
|
2944
|
+
/>
|
|
2945
|
+
) : null}
|
|
2946
|
+
{expandedStack && stackSpans.length > 0 ? (
|
|
2947
|
+
<CommentStackGuide top={stackGuideTop} height={stackGuideHeight} />
|
|
2948
|
+
) : null}
|
|
2457
2949
|
{/* Pending new comment — positioned at the selection Y offset */}
|
|
2458
2950
|
{displayedPendingComment && (
|
|
2459
2951
|
<div
|
|
2460
2952
|
ref={pendingCardRef}
|
|
2461
2953
|
className={cn(
|
|
2462
|
-
"overflow-hidden rounded-xl bg-popover ring-1 ring-border/60 shadow-comment-raised",
|
|
2463
2954
|
alignToAnchors
|
|
2464
|
-
?
|
|
2955
|
+
? composingStack
|
|
2956
|
+
? "absolute inset-x-0 z-10 pb-2"
|
|
2957
|
+
: "absolute left-2 right-4 z-10"
|
|
2465
2958
|
: surface === "popover"
|
|
2466
2959
|
? "relative"
|
|
2467
2960
|
: "relative mx-2 mt-3",
|
|
2468
2961
|
)}
|
|
2469
2962
|
style={alignToAnchors ? { top: pendingTop } : undefined}
|
|
2470
2963
|
data-comment-pending
|
|
2964
|
+
data-comment-stack={composingStack?.key}
|
|
2965
|
+
data-comment-stack-state={composingStack ? "composing" : undefined}
|
|
2471
2966
|
>
|
|
2472
|
-
{
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2967
|
+
{composingStack ? <CommentStackSheets /> : null}
|
|
2968
|
+
<div
|
|
2969
|
+
className={cn(
|
|
2970
|
+
"relative overflow-hidden rounded-xl bg-popover ring-1 ring-border/60 shadow-comment-raised",
|
|
2971
|
+
composingStack && "mx-2 mr-4",
|
|
2972
|
+
)}
|
|
2973
|
+
>
|
|
2974
|
+
{surface === "popover" ? (
|
|
2975
|
+
<div className="flex h-12 items-center gap-1 border-b border-border/70 pe-2 ps-4">
|
|
2976
|
+
<span className="min-w-0 flex-1 truncate text-sm font-semibold text-foreground">
|
|
2977
|
+
{t("comments.commentTitle")}
|
|
2978
|
+
</span>
|
|
2979
|
+
{onClose ? (
|
|
2980
|
+
<Tooltip>
|
|
2981
|
+
<TooltipTrigger asChild>
|
|
2982
|
+
<CommentIconButton
|
|
2983
|
+
aria-label={t("comments.close")}
|
|
2984
|
+
onClick={handlePendingCancel}
|
|
2985
|
+
>
|
|
2986
|
+
<IconX size={18} />
|
|
2987
|
+
</CommentIconButton>
|
|
2988
|
+
</TooltipTrigger>
|
|
2989
|
+
<TooltipContent>{t("comments.close")}</TooltipContent>
|
|
2990
|
+
</Tooltip>
|
|
2991
|
+
) : null}
|
|
2992
|
+
</div>
|
|
2993
|
+
) : null}
|
|
2994
|
+
<div className="flex items-start gap-2.5 p-3.5">
|
|
2995
|
+
<div className="flex h-9.5 shrink-0 items-center">
|
|
2996
|
+
<CommentAvatar
|
|
2997
|
+
email={currentUserEmail}
|
|
2998
|
+
name={currentUserEmail}
|
|
2504
2999
|
/>
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
3000
|
+
</div>
|
|
3001
|
+
<div className="min-w-0 flex-1">
|
|
3002
|
+
{displayedPendingComment.property ? (
|
|
3003
|
+
<PropertyThreadTargetLabel
|
|
3004
|
+
className="mb-2"
|
|
3005
|
+
target={propertyThreadTarget(
|
|
3006
|
+
displayedPendingComment.property,
|
|
3007
|
+
documentProperties,
|
|
3008
|
+
)}
|
|
3009
|
+
/>
|
|
3010
|
+
) : null}
|
|
3011
|
+
{!pendingTargetValid && (
|
|
3012
|
+
<p
|
|
3013
|
+
role="alert"
|
|
3014
|
+
className="mb-2 text-xs text-muted-foreground"
|
|
3015
|
+
>
|
|
3016
|
+
{t("comments.selectTextToComment")}
|
|
3017
|
+
</p>
|
|
3018
|
+
)}
|
|
3019
|
+
<CommentComposer
|
|
3020
|
+
ref={pendingInputRef}
|
|
3021
|
+
value={pendingText}
|
|
3022
|
+
onChange={(text) =>
|
|
3023
|
+
onPendingChange?.(displayedPendingComment.id, () => ({
|
|
3024
|
+
text,
|
|
3025
|
+
}))
|
|
3026
|
+
}
|
|
3027
|
+
mentions={pendingMentions}
|
|
3028
|
+
onMentionsChange={(mentions) =>
|
|
3029
|
+
onPendingChange?.(displayedPendingComment.id, () => ({
|
|
3030
|
+
mentions,
|
|
3031
|
+
}))
|
|
3032
|
+
}
|
|
3033
|
+
onSubmit={() => void handlePendingSubmit()}
|
|
3034
|
+
onAiSubmit={
|
|
3035
|
+
commentAi && !displayedPendingComment.fieldId
|
|
3036
|
+
? (selection) => void handlePendingAiSubmit(selection)
|
|
3037
|
+
: undefined
|
|
3038
|
+
}
|
|
3039
|
+
aiDraft={pendingDraft.draft.aiDraft}
|
|
3040
|
+
onAiDraftChange={pendingDraft.setAiDraft}
|
|
3041
|
+
aiModelStorageKey={
|
|
3042
|
+
commentAi && !displayedPendingComment.fieldId
|
|
3043
|
+
? aiModelStorageKey
|
|
3044
|
+
: undefined
|
|
3045
|
+
}
|
|
3046
|
+
onCancel={handlePendingCancel}
|
|
3047
|
+
onEscape={() => {
|
|
3048
|
+
if (!pendingText.trim()) handlePendingCancel();
|
|
3049
|
+
}}
|
|
3050
|
+
onFocus={() => {
|
|
3051
|
+
const selection = pendingInputRef.current?.getSelection();
|
|
3052
|
+
if (selection && pendingFocus)
|
|
3053
|
+
pendingFocus.current = selection;
|
|
3054
|
+
}}
|
|
3055
|
+
onSelectionChange={(selection) => {
|
|
3056
|
+
if (pendingFocus) pendingFocus.current = selection;
|
|
3057
|
+
}}
|
|
3058
|
+
onBlur={() => {
|
|
3059
|
+
if (pendingFocus) pendingFocus.current = null;
|
|
3060
|
+
}}
|
|
3061
|
+
placeholder={t("comments.add")}
|
|
3062
|
+
submitLabel={t("comments.submit")}
|
|
3063
|
+
submitDisabled={!pendingTargetValid || ambiguousCreate()}
|
|
3064
|
+
disabled={pendingSubmitting}
|
|
3065
|
+
/>
|
|
3066
|
+
</div>
|
|
2558
3067
|
</div>
|
|
3068
|
+
{composingStack ? (
|
|
3069
|
+
<CommentStackShowAll
|
|
3070
|
+
threads={composingStack.threads}
|
|
3071
|
+
onOpen={() => openStackOn(composingStack.threads[0].threadId)}
|
|
3072
|
+
t={t}
|
|
3073
|
+
/>
|
|
3074
|
+
) : null}
|
|
2559
3075
|
</div>
|
|
2560
3076
|
</div>
|
|
2561
3077
|
)}
|
|
@@ -2566,26 +3082,33 @@ export function CommentsSidebar({
|
|
|
2566
3082
|
const restingItem = restingItemsById.get(thread.threadId);
|
|
2567
3083
|
const marginTop = restingItem?.marginTop ?? item.marginTop;
|
|
2568
3084
|
const translateY = top - (restingItem?.top ?? top);
|
|
2569
|
-
const
|
|
3085
|
+
const stack =
|
|
2570
3086
|
"suggestion" in thread
|
|
2571
|
-
?
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
3087
|
+
? undefined
|
|
3088
|
+
: stackOfThread.get(thread.threadId);
|
|
3089
|
+
const card =
|
|
3090
|
+
stack && stack !== expandedStack
|
|
3091
|
+
? renderFoldedStack(stack, thread.threadId)
|
|
3092
|
+
: "suggestion" in thread
|
|
3093
|
+
? "durability" in thread.suggestion
|
|
3094
|
+
? renderDraftSuggestionCard(thread.suggestion)
|
|
3095
|
+
: thread.suggestion.proposalId &&
|
|
3096
|
+
(proposalMemberCounts.get(thread.suggestion.proposalId) ??
|
|
3097
|
+
0) >= 2
|
|
3098
|
+
? renderProposalGroup(
|
|
3099
|
+
thread.suggestion.proposalId,
|
|
3100
|
+
inlineProposalMembers.get(
|
|
3101
|
+
thread.suggestion.proposalId,
|
|
3102
|
+
) ?? [thread.suggestion],
|
|
3103
|
+
thread.threadId,
|
|
3104
|
+
)
|
|
3105
|
+
: renderSuggestionCard(thread.suggestion)
|
|
3106
|
+
: renderCommentThread(
|
|
3107
|
+
thread,
|
|
3108
|
+
0,
|
|
3109
|
+
activeThreadId === thread.threadId,
|
|
3110
|
+
!!stack,
|
|
3111
|
+
);
|
|
2589
3112
|
// A Property thread has no highlight to lose, so it isn't one that
|
|
2590
3113
|
// went missing.
|
|
2591
3114
|
const lostHighlight = (candidate: (typeof items)[number]) =>
|
|
@@ -2667,6 +3190,8 @@ function ProposalGroup({
|
|
|
2667
3190
|
totalCount,
|
|
2668
3191
|
members,
|
|
2669
3192
|
active,
|
|
3193
|
+
expanded,
|
|
3194
|
+
onExpandedChange,
|
|
2670
3195
|
deciding,
|
|
2671
3196
|
canDecide,
|
|
2672
3197
|
onDecide,
|
|
@@ -2679,6 +3204,8 @@ function ProposalGroup({
|
|
|
2679
3204
|
totalCount: number;
|
|
2680
3205
|
members: ResourceSuggestion[];
|
|
2681
3206
|
active: boolean;
|
|
3207
|
+
expanded: boolean;
|
|
3208
|
+
onExpandedChange: (expanded: boolean) => void;
|
|
2682
3209
|
deciding: boolean;
|
|
2683
3210
|
canDecide: boolean;
|
|
2684
3211
|
onDecide: (decision: SuggestionDecision) => void;
|
|
@@ -2686,11 +3213,10 @@ function ProposalGroup({
|
|
|
2686
3213
|
children: ReactNode;
|
|
2687
3214
|
t: ReturnType<typeof useT>;
|
|
2688
3215
|
}) {
|
|
2689
|
-
const [expanded, setExpanded] = useState(active);
|
|
2690
3216
|
const groupRef = useRef<HTMLDivElement>(null);
|
|
2691
3217
|
const detailsId = useId();
|
|
2692
|
-
|
|
2693
|
-
if (active)
|
|
3218
|
+
useLayoutEffect(() => {
|
|
3219
|
+
if (active) onExpandedChange(true);
|
|
2694
3220
|
}, [active]);
|
|
2695
3221
|
useLayoutEffect(() => {
|
|
2696
3222
|
const element = groupRef.current;
|
|
@@ -2714,7 +3240,7 @@ function ProposalGroup({
|
|
|
2714
3240
|
type="button"
|
|
2715
3241
|
aria-expanded={expanded}
|
|
2716
3242
|
aria-controls={detailsId}
|
|
2717
|
-
onClick={() =>
|
|
3243
|
+
onClick={() => onExpandedChange(!expanded)}
|
|
2718
3244
|
className="flex w-full min-w-0 items-start gap-2 px-3 py-2 text-start text-xs hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
|
|
2719
3245
|
>
|
|
2720
3246
|
<IconChevronDown
|
|
@@ -2733,7 +3259,9 @@ function ProposalGroup({
|
|
|
2733
3259
|
{summary}
|
|
2734
3260
|
</span>
|
|
2735
3261
|
<span className="shrink-0 text-muted-foreground">
|
|
2736
|
-
{t("comments.proposalEditCount", {
|
|
3262
|
+
{t("comments.proposalEditCount", {
|
|
3263
|
+
count: pending.length || totalCount,
|
|
3264
|
+
})}
|
|
2737
3265
|
</span>
|
|
2738
3266
|
</button>
|
|
2739
3267
|
{expanded ? (
|
|
@@ -2796,6 +3324,19 @@ export function agentSuggestionReason(
|
|
|
2796
3324
|
return reason;
|
|
2797
3325
|
}
|
|
2798
3326
|
|
|
3327
|
+
/**
|
|
3328
|
+
* A change that adds or removes a line break, or swaps one run of whitespace
|
|
3329
|
+
* for another, doesn't say which lines it joins or splits, and a break alone
|
|
3330
|
+
* reads as a lone "↵", so what it replaces is shown too.
|
|
3331
|
+
*/
|
|
3332
|
+
function changesLayout(removed: string, inserted: string) {
|
|
3333
|
+
const before = suggestionAnchorText(removed);
|
|
3334
|
+
const after = suggestionAnchorText(inserted);
|
|
3335
|
+
return (
|
|
3336
|
+
/\n/.test(before + after) || (/^\s+$/.test(before) && /^\s+$/.test(after))
|
|
3337
|
+
);
|
|
3338
|
+
}
|
|
3339
|
+
|
|
2799
3340
|
function SuggestionOperationSummary({
|
|
2800
3341
|
operations,
|
|
2801
3342
|
expanded,
|
|
@@ -2851,6 +3392,18 @@ function SuggestionOperationSummary({
|
|
|
2851
3392
|
: null;
|
|
2852
3393
|
const hasSharedContext = diff?.some((part) => part.type === "equal");
|
|
2853
3394
|
const richText = /[<>*_`\[\]]/.test(previousText + nextText);
|
|
3395
|
+
const changed = (type: "delete" | "insert", whole: string) =>
|
|
3396
|
+
diff && hasSharedContext
|
|
3397
|
+
? diff
|
|
3398
|
+
.flatMap((part) => (part.type === type ? [part.text] : []))
|
|
3399
|
+
.join("")
|
|
3400
|
+
: whole;
|
|
3401
|
+
const showReplaced =
|
|
3402
|
+
expanded ||
|
|
3403
|
+
changesLayout(
|
|
3404
|
+
changed("delete", previousText),
|
|
3405
|
+
changed("insert", nextText),
|
|
3406
|
+
);
|
|
2854
3407
|
const renderDiff = (side: "before" | "after") => {
|
|
2855
3408
|
let offset = 0;
|
|
2856
3409
|
const presentation =
|
|
@@ -2879,7 +3432,9 @@ function SuggestionOperationSummary({
|
|
|
2879
3432
|
to: from + part.text.length,
|
|
2880
3433
|
},
|
|
2881
3434
|
)
|
|
2882
|
-
: part.text
|
|
3435
|
+
: part.type !== "equal" && /^\s+$/.test(part.text)
|
|
3436
|
+
? suggestionTextForDisplay(part.text)
|
|
3437
|
+
: part.text}
|
|
2883
3438
|
</span>
|
|
2884
3439
|
);
|
|
2885
3440
|
});
|
|
@@ -2897,7 +3452,7 @@ function SuggestionOperationSummary({
|
|
|
2897
3452
|
: renderSuggestionText(nextText, nextPresentation)}
|
|
2898
3453
|
{"”"}
|
|
2899
3454
|
</div>
|
|
2900
|
-
{
|
|
3455
|
+
{showReplaced ? (
|
|
2901
3456
|
<div className="text-muted-foreground">
|
|
2902
3457
|
{t("comments.suggestionReplace")}: {"“"}
|
|
2903
3458
|
{diff && hasSharedContext
|
|
@@ -2947,11 +3502,13 @@ function DraftSuggestionThreadView({
|
|
|
2947
3502
|
surface = "rail",
|
|
2948
3503
|
onClose,
|
|
2949
3504
|
currentUserEmail,
|
|
3505
|
+
currentUserName,
|
|
2950
3506
|
t,
|
|
2951
3507
|
}: {
|
|
2952
3508
|
surface?: CommentSurface;
|
|
2953
3509
|
onClose?: () => void;
|
|
2954
3510
|
currentUserEmail?: string;
|
|
3511
|
+
currentUserName?: string;
|
|
2955
3512
|
marginTop?: number;
|
|
2956
3513
|
onHeightChange: (threadId: string, height: number) => void;
|
|
2957
3514
|
suggestion: DraftSuggestion;
|
|
@@ -2984,7 +3541,11 @@ function DraftSuggestionThreadView({
|
|
|
2984
3541
|
{
|
|
2985
3542
|
id: suggestion.id,
|
|
2986
3543
|
author_email: suggestion.authorEmail ?? "",
|
|
2987
|
-
author_name:
|
|
3544
|
+
author_name: ownAuthorName(
|
|
3545
|
+
suggestion.authorEmail,
|
|
3546
|
+
currentUserEmail,
|
|
3547
|
+
currentUserName,
|
|
3548
|
+
),
|
|
2988
3549
|
created_at: suggestion.createdAt,
|
|
2989
3550
|
content: "",
|
|
2990
3551
|
mentions: [],
|
|
@@ -3102,11 +3663,13 @@ function SuggestionThreadView({
|
|
|
3102
3663
|
surface = "rail",
|
|
3103
3664
|
onClose,
|
|
3104
3665
|
currentUserEmail,
|
|
3666
|
+
currentUserName,
|
|
3105
3667
|
t,
|
|
3106
3668
|
}: {
|
|
3107
3669
|
surface?: CommentSurface;
|
|
3108
3670
|
onClose?: () => void;
|
|
3109
3671
|
currentUserEmail?: string;
|
|
3672
|
+
currentUserName?: string;
|
|
3110
3673
|
compact: boolean;
|
|
3111
3674
|
replyDrafts: ReturnType<typeof useCommentReplyDrafts>;
|
|
3112
3675
|
marginTop?: number;
|
|
@@ -3182,6 +3745,11 @@ function SuggestionThreadView({
|
|
|
3182
3745
|
author_email: suggestion.authorEmail ?? "",
|
|
3183
3746
|
author_name:
|
|
3184
3747
|
root?.authorName ??
|
|
3748
|
+
ownAuthorName(
|
|
3749
|
+
suggestion.authorEmail,
|
|
3750
|
+
currentUserEmail,
|
|
3751
|
+
currentUserName,
|
|
3752
|
+
) ??
|
|
3185
3753
|
(suggestion.authorEmail ? null : suggestion.actorKind),
|
|
3186
3754
|
created_at: suggestion.createdAt,
|
|
3187
3755
|
content: "",
|
|
@@ -3192,6 +3760,11 @@ function SuggestionThreadView({
|
|
|
3192
3760
|
author_email: comment.authorEmail ?? "",
|
|
3193
3761
|
author_name:
|
|
3194
3762
|
comment.authorName ??
|
|
3763
|
+
ownAuthorName(
|
|
3764
|
+
comment.authorEmail,
|
|
3765
|
+
currentUserEmail,
|
|
3766
|
+
currentUserName,
|
|
3767
|
+
) ??
|
|
3195
3768
|
(comment.authorEmail ? null : comment.createdBy),
|
|
3196
3769
|
created_at: comment.createdAt,
|
|
3197
3770
|
content: comment.body,
|
|
@@ -3476,6 +4049,7 @@ function ThreadView({
|
|
|
3476
4049
|
renderEntry,
|
|
3477
4050
|
surface = "rail",
|
|
3478
4051
|
quote,
|
|
4052
|
+
showQuote = false,
|
|
3479
4053
|
propertyTarget,
|
|
3480
4054
|
anchorUnavailable = false,
|
|
3481
4055
|
onClose,
|
|
@@ -3522,6 +4096,8 @@ function ThreadView({
|
|
|
3522
4096
|
surface?: CommentSurface;
|
|
3523
4097
|
/** The anchored text, shown as a quote line above the thread in the panel. */
|
|
3524
4098
|
quote?: string | null;
|
|
4099
|
+
/** Show the quote in the margin too, for a card away from its highlight. */
|
|
4100
|
+
showQuote?: boolean;
|
|
3525
4101
|
/** The Property a thread is on, shown above it on every surface. */
|
|
3526
4102
|
propertyTarget?: PropertyThreadTarget | null;
|
|
3527
4103
|
anchorUnavailable?: boolean;
|
|
@@ -3786,18 +4362,33 @@ function ThreadView({
|
|
|
3786
4362
|
target={propertyTarget}
|
|
3787
4363
|
/>
|
|
3788
4364
|
) : null}
|
|
3789
|
-
{isPanel && quote ? (
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
event
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
4365
|
+
{(isPanel || showQuote) && quote ? (
|
|
4366
|
+
canExpand ? (
|
|
4367
|
+
<button
|
|
4368
|
+
type="button"
|
|
4369
|
+
onClick={(event) => {
|
|
4370
|
+
event.stopPropagation();
|
|
4371
|
+
onExpand();
|
|
4372
|
+
}}
|
|
4373
|
+
className={cn(
|
|
4374
|
+
"-mb-1 border-s-2 border-border ps-2.5 text-start text-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
4375
|
+
isPanel ? "line-clamp-2" : "line-clamp-1",
|
|
4376
|
+
)}
|
|
4377
|
+
data-comment-quote
|
|
4378
|
+
>
|
|
4379
|
+
{quote}
|
|
4380
|
+
</button>
|
|
4381
|
+
) : (
|
|
4382
|
+
<p
|
|
4383
|
+
className={cn(
|
|
4384
|
+
"-mb-1 border-s-2 border-border ps-2.5 text-sm text-muted-foreground",
|
|
4385
|
+
isPanel ? "line-clamp-2" : "line-clamp-1",
|
|
4386
|
+
)}
|
|
4387
|
+
data-comment-quote
|
|
4388
|
+
>
|
|
4389
|
+
{quote}
|
|
4390
|
+
</p>
|
|
4391
|
+
)
|
|
3801
4392
|
) : null}
|
|
3802
4393
|
{anchorUnavailable ? (
|
|
3803
4394
|
<span
|