mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +00:00
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>
199 lines
6.1 KiB
TypeScript
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);
|
|
}
|
|
}
|