Skip to content

Commit b4dd19c

Browse files
committed
fix(desktop): fence forum clipboard settlement before invitation
Preserve the inherited clipboard wait through restack and gate its pre-preparation continuation by mounted visit and authored revision. Adapt the real composer harness to the clipboard seam and cover late settlement across edit, navigation, return and unmount. Signed-off-by: Logan Johnson <loganj@squareup.com>
1 parent e058185 commit b4dd19c

2 files changed

Lines changed: 37 additions & 0 deletions

File tree

desktop/src/features/forum/ui/ForumComposer.lifecycle.test.mjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ async function setup(options = {}) {
121121
"@/features/messages/lib/normalizeMentionClipboard": {
122122
hasMentionClipboardHtml: () => false,
123123
},
124+
"@/features/messages/lib/mentionClipboardPaste": {
125+
handleMentionClipboardPaste: () => false,
126+
},
124127
"@/features/messages/lib/useLinkEditor": { useLinkEditor: () => ({}) },
125128
"./useCompactComposerInteractions": {
126129
useCompactComposerInteractions: () => ({ shouldIgnoreBlur: () => false }),
@@ -131,6 +134,9 @@ async function setup(options = {}) {
131134
mentionState = state.current;
132135
return {
133136
knownNames: {},
137+
settlePendingMentionBindings: async () => {
138+
if (control.settle) await control.settle.promise;
139+
},
134140
cancelMentionAutocomplete: noop,
135141
updateMentionQuery: noop,
136142
clearMentions: () => {
@@ -544,3 +550,26 @@ for (const result of ["failure", "success"]) {
544550
assert.equal(s.calls.filter((c) => c[0] === "add").length, 1);
545551
});
546552
}
553+
554+
for (const action of ["navigation", "return", "edit", "unmount"]) {
555+
test(`clipboard settlement after ${action} cannot prepare another forum draft`, async () => {
556+
const s = await setup();
557+
s.edit();
558+
const gate = deferred();
559+
s.control.settle = gate;
560+
await s.submit();
561+
if (action === "navigation" || action === "return") {
562+
s.navigate("b");
563+
s.edit("B draft", []);
564+
if (action === "return") s.navigate("a");
565+
}
566+
if (action === "edit") s.edit("new authored draft", []);
567+
if (action === "unmount") s.unmount();
568+
await s.finish(gate);
569+
assert.equal(s.calls.length, 0);
570+
assert.equal(s.prompt.open, false);
571+
if (action === "navigation") assert.equal(s.text, "B draft");
572+
if (action === "return") assert.equal(s.text, TEXT);
573+
if (action === "edit") assert.equal(s.text, "new authored draft");
574+
});
575+
}

desktop/src/features/forum/ui/ForumComposer.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ function ForumComposerVisit({
340340
}
341341

342342
claimDraftSend(draftKey);
343+
const composerRevision = draftLifecycle.getComposerRevision();
343344
isSubmissionPendingRef.current = true;
344345
setIsSubmissionPending(true);
345346
mentions.cancelMentionAutocomplete();
@@ -349,6 +350,12 @@ function ForumComposerVisit({
349350
// A pasted mention's identity check can still be in flight; extracting
350351
// first would publish the label with no `p` tag. Bounded internally.
351352
await mentions.settlePendingMentionBindings();
353+
// This await precedes the preparation adapter's own visit fence.
354+
if (
355+
!mountedRef.current ||
356+
draftLifecycle.getComposerRevision() !== composerRevision
357+
)
358+
return;
352359
const pubkeys = await prepareMentionPubkeys(
353360
mentions.extractMentionPubkeys(trimmed),
354361
trimmed,
@@ -406,6 +413,7 @@ function ForumComposerVisit({
406413
draftKey,
407414
drafts.clearDraft,
408415
draftLifecycle.runComposerUpdate,
416+
draftLifecycle.getComposerRevision,
409417
mentions.getDraftMentionRefs,
410418
captureRecovery,
411419
media.pendingImetaRef,

0 commit comments

Comments
 (0)