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 <br /> 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 <br /> 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 <br />)

* 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 <noreply@anthropic.com>
This commit is contained in:
Alejandro
2026-01-13 20:50:46 +01:00
committed by GitHub
parent 4078ea372a
commit 1356afe9ea
2 changed files with 32 additions and 22 deletions

View File

@@ -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;
}
},
};
},

View File

@@ -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 ? (
<br />
) : idx === 0 || idx === lines.length - 1 ? (
<span dir="auto">{line}</span> // FIXME: this should be span or div depnding on context
) : (
<div dir="auto" key={idx}>
{line}
</div>
),
)}
</>
);
// If no newlines, render as simple span
if (!text.includes("\n")) {
return <span dir="auto">{text}</span>;
}
return <span dir="auto">{text}</span>;
// Multi-line text: split and render with <br /> between lines
const lines = text.split("\n");
return (
<>
{lines.map((line, idx) => (
<span key={idx} dir="auto">
{line}
{idx < lines.length - 1 && <br />}
</span>
))}
</>
);
}