Files
buzz/desktop/src/features/messages/ui/submitMessageEdit.ts
Logan Johnson cc096ca2a1 fix(desktop): preserve mention competitors through fallbacks
Compose edit fallback refs before exact occurrence selection. Keep ambiguous historical labels as non-authorizing blockers for rendering and body-based recipient fallbacks; qualified labels only disambiguate when they own an unambiguous occurrence.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
(cherry picked from commit 30545075a876136467c07d82abac83ad1904df23)
Signed-off-by: Logan Johnson <loganj@squareup.com>
(cherry picked from commit df75b5d1ac6d7aaa2bf0dfb4dfaf43b92539249a)
Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-09-01 11:55:36 -04:00

199 lines
6.1 KiB
TypeScript

import {
AgentMentionAuthorizationError,
type MentionRevalidationOptions,
} from "@/features/messages/lib/agentMentionRevalidation";
import type { QueuedMediaAttachment } from "@/features/messages/lib/backgroundMediaUploadStore";
import { enqueueBackgroundMediaUpload } from "@/features/messages/lib/backgroundMediaUploadStore";
import type { DraftMentionRef } from "@/features/messages/lib/useDrafts";
import type { MessageComposerEditTarget } from "@/features/messages/ui/MessageComposer.types";
import {
buildOutgoingMessage,
type ImetaMedia,
mergeOutgoingTags,
} from "@/features/messages/lib/imetaMediaMarkdown";
import { diffAddedMentionPubkeys } from "@/features/messages/lib/threading";
import { mergeOutgoingTagsWithReferenceMentions } from "@/features/messages/ui/useMentionSendFlow.helpers";
import { buildCustomEmojiTags } from "@/shared/lib/customEmojiTags";
import type { CustomEmoji } from "@/shared/lib/remarkCustomEmoji";
type EditDraft = {
content: string;
mentionRefs: DraftMentionRef[];
pendingImeta: ImetaMedia[];
queuedAttachments: QueuedMediaAttachment[];
spoileredAttachmentUrls: Set<string>;
unresolvedMentionPubkeys: string[];
};
type SubmitMessageEditOptions = Omit<
EditDraft,
"mentionRefs" | "unresolvedMentionPubkeys"
> & {
clearComposer: () => void;
customEmoji: ReadonlyArray<CustomEmoji>;
extractMentionPubkeys: (content: string) => string[];
getMentionRefs: (
content: string,
fallbackRefs: readonly DraftMentionRef[],
) => DraftMentionRef[];
editTargetId: string;
enqueueUpload?: typeof enqueueBackgroundMediaUpload;
editTarget: Pick<
MessageComposerEditTarget,
"mentionRefs" | "unresolvedMentionPubkeys"
>;
originalContent: string;
ownerPubkey: string | null;
restoreComposer: (draft: EditDraft) => void;
restoreMentionRefs: (refs: DraftMentionRef[]) => void;
revalidateMentionPubkeys: (
pubkeys: readonly string[],
channelId?: string | null,
options?: MentionRevalidationOptions,
) => Promise<string[]>;
shouldRestoreComposer: () => boolean;
setDeferredUploadPending: (isPending: boolean) => void;
save: (
content: string,
mediaTags?: string[][],
mentionPubkeys?: string[],
eventId?: string,
) => Promise<void>;
setUploadError: (message: string) => void;
};
export async function submitMessageEdit({
clearComposer,
content,
customEmoji,
editTargetId,
enqueueUpload = enqueueBackgroundMediaUpload,
editTarget,
extractMentionPubkeys,
getMentionRefs,
originalContent,
ownerPubkey,
pendingImeta,
queuedAttachments,
restoreComposer,
restoreMentionRefs,
revalidateMentionPubkeys,
setDeferredUploadPending,
shouldRestoreComposer,
save,
setUploadError,
spoileredAttachmentUrls,
}: SubmitMessageEditOptions): Promise<void> {
const draft: EditDraft = {
content,
mentionRefs: getMentionRefs(content, editTarget.mentionRefs ?? []),
pendingImeta: [...pendingImeta],
queuedAttachments: [...queuedAttachments],
spoileredAttachmentUrls: new Set(spoileredAttachmentUrls),
unresolvedMentionPubkeys: [...(editTarget.unresolvedMentionPubkeys ?? [])],
};
const restoreDraft = () => {
if (shouldRestoreComposer()) {
restoreComposer(draft);
restoreMentionRefs(draft.mentionRefs);
}
};
let originalMentionPubkeys: string[] = [];
try {
originalMentionPubkeys = extractMentionPubkeys(originalContent);
} catch {
// An old ambiguous label must not prevent removing it. If the original
// cannot be resolved, conservatively revalidate every current recipient.
}
let addedMentionPubkeys: string[];
try {
addedMentionPubkeys = diffAddedMentionPubkeys(
originalMentionPubkeys,
extractMentionPubkeys(content),
ownerPubkey ?? "",
);
} catch (error) {
setUploadError(error instanceof Error ? error.message : String(error));
return;
}
const hasQueuedAttachments = draft.queuedAttachments.length > 0;
if (hasQueuedAttachments) setDeferredUploadPending(true);
clearComposer();
const finishEdit = async (uploaded: ImetaMedia[], signal?: AbortSignal) => {
// An explicit empty media tag set tells edit receivers to wipe attachments.
const { content: finalContent, mediaTags } = buildOutgoingMessage(
content,
[...draft.pendingImeta, ...uploaded],
new Set([
...draft.spoileredAttachmentUrls,
...draft.queuedAttachments.flatMap((attachment, index) =>
attachment.spoilered && uploaded[index] ? [uploaded[index].url] : [],
),
]),
);
const outgoingTags = mergeOutgoingTagsWithReferenceMentions(
mergeOutgoingTags(
mediaTags,
buildCustomEmojiTags(finalContent, customEmoji),
),
[
...draft.mentionRefs.map(({ pubkey }) => pubkey),
...draft.unresolvedMentionPubkeys,
],
);
if (signal?.aborted) return;
const revalidatedMentionPubkeys = await revalidateMentionPubkeys(
addedMentionPubkeys,
undefined,
{
intendedAgentPubkeys: draft.mentionRefs
.filter((ref) => ref.isAgent)
.map((ref) => ref.pubkey),
},
);
if (signal?.aborted) return;
await save(
finalContent,
outgoingTags,
revalidatedMentionPubkeys,
editTargetId,
);
};
if (hasQueuedAttachments) {
enqueueUpload({
attachments: draft.queuedAttachments,
onComplete: async (uploaded, signal) => {
try {
await finishEdit(uploaded, signal);
} catch (error) {
restoreDraft();
if (error instanceof AgentMentionAuthorizationError)
setUploadError(error.message);
} finally {
setDeferredUploadPending(false);
}
},
onError: (error) => {
restoreDraft();
setUploadError(String(error));
setDeferredUploadPending(false);
},
onCancel: () => {
restoreDraft();
setDeferredUploadPending(false);
},
});
return;
}
try {
await finishEdit([]);
} catch (error) {
restoreDraft();
if (error instanceof AgentMentionAuthorizationError)
setUploadError(error.message);
}
}