mirror of
https://github.com/block/buzz.git
synced 2026-10-27 20:41:57 +00:00
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:
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user