fix(desktop): editor bug fixes — codeblock escaping, thread edit leak, channel links, and edit cancel (#553)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Wes
2026-05-12 12:35:10 -07:00
committed by GitHub
parent 61c30add40
commit 78508ea6cb
4 changed files with 37 additions and 12 deletions

View File

@@ -224,6 +224,17 @@ export const ChannelPane = React.memo(function ChannelPane({
const canResetThreadPanelWidth =
threadPanelWidthPx !== THREAD_PANEL_DEFAULT_WIDTH_PX;
// Scope the edit target to the correct composer: if the message being edited
// lives inside the open thread (thread head or a reply), show the editing UI
// only in the thread panel; otherwise show it in the main channel composer.
const isEditInThread =
editTarget != null &&
threadHeadMessage != null &&
(editTarget.id === threadHeadMessage.id ||
threadMessages.some((entry) => entry.message.id === editTarget.id));
const mainEditTarget = editTarget && !isEditInThread ? editTarget : null;
const threadEditTarget = editTarget && isEditInThread ? editTarget : null;
const isNonMemberView =
activeChannel !== null &&
!activeChannel.isMember &&
@@ -361,7 +372,7 @@ export const ChannelPane = React.memo(function ChannelPane({
channelId={activeChannel?.id ?? null}
channelName={activeChannel?.name ?? "channel"}
disabled={isComposerDisabled}
editTarget={editTarget}
editTarget={mainEditTarget}
isSending={isSending}
onCancelEdit={onCancelEdit}
onEditSave={onEditSave}
@@ -410,7 +421,7 @@ export const ChannelPane = React.memo(function ChannelPane({
channelName={activeChannel?.name ?? "channel"}
currentPubkey={currentPubkey}
disabled={isComposerDisabled}
editTarget={editTarget}
editTarget={threadEditTarget}
isSending={isSending}
onCancelEdit={onCancelEdit}
onCancelReply={onCancelThreadReply}

View File

@@ -426,6 +426,11 @@ function getMarkdownFromEditor(editor: Editor): string {
// line break syntax). Chat messages are plain text, not rendered markdown,
// so strip the backslashes to keep clean newlines.
md = md.replace(/\\\n/g, "\n");
// prosemirror-markdown's esc() backslash-escapes markdown special characters
// (` * \ ~ [ ] _) in text nodes to prevent them from being interpreted as
// formatting. Since our messages ARE rendered as markdown, we want to
// preserve the user's original characters so code fences, bold, etc. work.
md = md.replace(/\\([`*\\~[\]_])/g, "$1");
return md;
}
// Fallback: plain text

View File

@@ -98,7 +98,7 @@ export function MessageComposer({
const drafts = useDrafts();
const effectiveDraftKey = draftKey ?? channelId;
const previousDraftKeyRef = React.useRef<string | null>(null);
const preEditContentRef = React.useRef<string | null>(null);
const mentions = useMentions(channelId, undefined, profiles);
const channelLinks = useChannelLinks();
const emojiAutocomplete = useEmojiAutocomplete();
@@ -127,7 +127,6 @@ export function MessageComposer({
editTargetRef.current = editTarget;
channelIdRef.current = channelId;
// ── Refs consumed by Tiptap's submitOnEnter extension ──────────────
const isAutocompleteOpenRef = React.useRef(false);
isAutocompleteOpenRef.current =
mentions.isMentionOpen ||
@@ -136,7 +135,6 @@ export function MessageComposer({
const submitMessageRef = React.useRef<() => void>(() => {});
// ── Computed placeholder ─────────────────────────────────────────────
const computedPlaceholder = editTarget
? "Edit your message"
: (placeholder ??
@@ -204,14 +202,22 @@ export function MessageComposer({
};
}, [effectiveDraftKey]);
// ── Edit mode: pre-fill content ─────────────────────────────────────
// ── Edit mode: pre-fill content & restore on cancel ─────────────────
// biome-ignore lint/correctness/useExhaustiveDependencies: editTarget?.id is the trigger
React.useEffect(() => {
if (!editTarget) return;
setContent(editTarget.body);
contentRef.current = editTarget.body;
richText.setContent(editTarget.body);
richText.focus();
if (editTarget) {
preEditContentRef.current = contentRef.current;
setContent(editTarget.body);
contentRef.current = editTarget.body;
richText.setContent(editTarget.body);
richText.focus();
} else if (preEditContentRef.current !== null) {
const restored = preEditContentRef.current;
preEditContentRef.current = null;
setContent(restored);
contentRef.current = restored;
restored ? richText.setContent(restored) : richText.clearContent();
}
}, [editTarget?.id]);
// ── Focus on reply ──────────────────────────────────────────────────

View File

@@ -38,7 +38,10 @@ function walkChildren(node: any, pattern: RegExp, buildNode: NodeBuilder) {
if (child.type === "text") {
const parts = splitByPattern(child.value, pattern, buildNode);
if (parts.length > 1) {
if (
parts.length > 1 ||
(parts.length === 1 && parts[0].type !== "text")
) {
node.children.splice(i, 1, ...parts);
}
} else {