From 1356afe9ea8d541f6f1f5fc7a174711f3fadf7c8 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Tue, 13 Jan 2026 20:50:46 +0100 Subject: [PATCH] Fix newline rendering in chat messages (#80) * Fix newline rendering in chat messages The Text component was not properly rendering newlines in chat messages. The previous implementation had buggy logic that only rendered
for empty lines and used an inconsistent mix of spans and divs for non-empty lines, which didn't create proper line breaks between consecutive text lines. Changes: - Render each line in a span with
between consecutive lines - Remove unused useMemo import and fix React hooks violation - Simplify logic for better maintainability This ensures that multi-line messages in chat (and other text content) display correctly with proper line breaks. Fixes rendering of newlines in NIP-29 groups and NIP-53 live chat. * Preserve newlines when sending chat messages The MentionEditor's serializeContent function was not handling hardBreak nodes created by Shift+Enter. This caused newlines within messages to be lost during serialization, even though the editor displayed them correctly. Changes: - Add hardBreak node handling in serializeContent - Preserve newlines (\n) from Shift+Enter keypresses - Ensure multi-line messages are sent with proper line breaks With this fix and the previous Text.tsx fix, newlines are now properly: 1. Captured when typing (Shift+Enter creates hardBreak) 2. Preserved when sending (hardBreak serialized as \n) 3. Rendered when displaying (Text component renders \n as
) * Make Enter insert newline on mobile devices On mobile devices, pressing Enter now inserts a newline (hardBreak) instead of submitting the message. This provides better UX since mobile keyboards don't have easy access to Shift+Enter for multiline input. Behavior: - Desktop: Enter submits, Shift+Enter inserts newline (unchanged) - Mobile: Enter inserts newline, Cmd/Ctrl+Enter submits - Mobile detection: Uses touch support API (ontouchstart or maxTouchPoints) Users can still submit messages on mobile using: 1. The Send button (primary method) 2. Ctrl+Enter keyboard shortcut (if available) --------- Co-authored-by: Claude --- src/components/editor/MentionEditor.tsx | 18 ++++++++++--- src/components/nostr/RichText/Text.tsx | 36 ++++++++++++------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/components/editor/MentionEditor.tsx b/src/components/editor/MentionEditor.tsx index d301565..7fadd8a 100644 --- a/src/components/editor/MentionEditor.tsx +++ b/src/components/editor/MentionEditor.tsx @@ -584,6 +584,9 @@ export const MentionEditor = forwardRef< node.content?.forEach((child: any) => { if (child.type === "text") { text += child.text; + } else if (child.type === "hardBreak") { + // Preserve newlines from Shift+Enter + text += "\n"; } else if (child.type === "mention") { const pubkey = child.attrs?.id; if (pubkey) { @@ -664,6 +667,9 @@ export const MentionEditor = forwardRef< // Build extensions array const extensions = useMemo(() => { + // Detect mobile devices (touch support) + const isMobile = "ontouchstart" in window || navigator.maxTouchPoints > 0; + // Custom extension for keyboard shortcuts (runs before suggestion plugins) const SubmitShortcut = Extension.create({ name: "submitShortcut", @@ -674,10 +680,16 @@ export const MentionEditor = forwardRef< handleSubmitRef.current(editor); return true; }, - // Plain Enter submits (Shift+Enter handled by hardBreak for newlines) + // Plain Enter behavior depends on device Enter: ({ editor }) => { - handleSubmitRef.current(editor); - return true; + if (isMobile) { + // On mobile, Enter inserts a newline (hardBreak) + return editor.commands.setHardBreak(); + } else { + // On desktop, Enter submits the message + handleSubmitRef.current(editor); + return true; + } }, }; }, diff --git a/src/components/nostr/RichText/Text.tsx b/src/components/nostr/RichText/Text.tsx index 12a03eb..7ac4823 100644 --- a/src/components/nostr/RichText/Text.tsx +++ b/src/components/nostr/RichText/Text.tsx @@ -1,5 +1,4 @@ import { CommonData } from "applesauce-content/nast"; -import { useMemo } from "react"; interface TextNodeProps { node: { @@ -11,23 +10,22 @@ interface TextNodeProps { export function Text({ node }: TextNodeProps) { const text = node.value; - const lines = useMemo(() => text.split("\n"), [text]); - if (text.includes("\n")) { - return ( - <> - {lines.map((line, idx) => - line.trim().length === 0 ? ( -
- ) : idx === 0 || idx === lines.length - 1 ? ( - {line} // FIXME: this should be span or div depnding on context - ) : ( -
- {line} -
- ), - )} - - ); + + // If no newlines, render as simple span + if (!text.includes("\n")) { + return {text}; } - return {text}; + + // Multi-line text: split and render with
between lines + const lines = text.split("\n"); + return ( + <> + {lines.map((line, idx) => ( + + {line} + {idx < lines.length - 1 &&
} +
+ ))} + + ); }