Use inline WYSIWYG markdown editor for long text messages
Desktop: replace the Write/Preview tab toggle in the compose card with MDXEditor, an inline WYSIWYG that renders markdown as you type (Obsidian/ Notion feel) and round-trips plain markdown, matching how particle content is stored. Immersive mode is unchanged — short, plain notes still get the centered large-type textarea, and ⌘+M still drops into the rich editor. Shortcut handling moves to the capture phase so ⌘+Enter / Esc win over the editor's own key handling. Mobile: render markdown on the display side via react-native-marked's useMarkdown hook (no FlatList, so it nests cleanly in the existing ScrollView). Mirrors desktop's immersive-vs-card logic: short plain notes stay centered; anything with markdown renders formatted instead of showing raw syntax. Composer stays plain text. https://claude.ai/code/session_019DU6V5z6Nr4Vu7b1fBDnq4
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ScrollView, Text, View } from "react-native";
|
||||
import { useMarkdown, type MarkedStyles } from "react-native-marked";
|
||||
import type { Particle } from "@/api/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RelativeTimestamp } from "@/components/RelativeTimestamp";
|
||||
@@ -37,6 +38,32 @@ function getImmersiveStyle(length: number) {
|
||||
return { className: "text-2xl font-normal leading-snug" };
|
||||
}
|
||||
|
||||
// Mirrors desktop's text-particle-view: short plain notes get the immersive
|
||||
// centered treatment; anything with markdown syntax renders formatted instead
|
||||
// of showing raw `**asterisks**`.
|
||||
function hasMarkdownFormatting(content: string): boolean {
|
||||
return /^#{1,6} |^\s*[-*+] |^\s*\d+\. |^```|`[^`]+`|\*\*|__|\*[^*]|_[^_]|^>/m.test(
|
||||
content,
|
||||
);
|
||||
}
|
||||
|
||||
// Dark theme + base typography for rendered markdown. Defined at module scope
|
||||
// so the references stay stable — `useMarkdown` re-parses only when these or
|
||||
// the content change.
|
||||
const MARKDOWN_THEME = {
|
||||
colors: {
|
||||
text: "#ffffff",
|
||||
link: "#60a5fa",
|
||||
code: "rgba(255,255,255,0.1)",
|
||||
border: "rgba(255,255,255,0.2)",
|
||||
},
|
||||
};
|
||||
|
||||
const MARKDOWN_STYLES: MarkedStyles = {
|
||||
text: { color: "#ffffff", fontSize: 18, lineHeight: 28 },
|
||||
codespan: { color: "#ffffff" },
|
||||
};
|
||||
|
||||
export function TextParticleView({
|
||||
particle,
|
||||
paused,
|
||||
@@ -48,6 +75,10 @@ export function TextParticleView({
|
||||
const durationS = computeReadDuration(content);
|
||||
const elapsedRef = useRef(0);
|
||||
const safe = useStreamSafeArea();
|
||||
const markdownNodes = useMarkdown(content, {
|
||||
theme: MARKDOWN_THEME,
|
||||
styles: MARKDOWN_STYLES,
|
||||
});
|
||||
|
||||
const editedLabel = editedAt ? (
|
||||
<View className="mt-3 items-center">
|
||||
@@ -79,8 +110,10 @@ export function TextParticleView({
|
||||
return () => clearInterval(interval);
|
||||
}, [paused, durationS, onEnded, onProgress, particle.id]);
|
||||
|
||||
// Immersive (short, plain): centered, large type — feels like a lock-screen note.
|
||||
if (content.length < IMMERSIVE_CHAR_LIMIT) {
|
||||
// Immersive (short, plain): centered, large type — feels like a lock-screen
|
||||
// note. Short messages that contain markdown fall through to the rendered
|
||||
// card so formatting isn't shown as raw syntax.
|
||||
if (content.length < IMMERSIVE_CHAR_LIMIT && !hasMarkdownFormatting(content)) {
|
||||
const style = getImmersiveStyle(content.length);
|
||||
return (
|
||||
<View
|
||||
@@ -100,10 +133,12 @@ export function TextParticleView({
|
||||
);
|
||||
}
|
||||
|
||||
// Long text: scrollable card so the reader can pace themselves; the
|
||||
// duration timer keeps ticking either way, which is intentional —
|
||||
// long messages should still auto-advance at the 15s cap. Padding is
|
||||
// pulled from the StreamSafeArea so the card never slips under chrome.
|
||||
// Long text or markdown: scrollable card so the reader can pace themselves;
|
||||
// the duration timer keeps ticking either way, which is intentional — long
|
||||
// messages should still auto-advance at the 15s cap. Markdown is rendered
|
||||
// via the useMarkdown hook (not the FlatList-based component) so its blocks
|
||||
// nest cleanly inside this ScrollView. Padding is pulled from the
|
||||
// StreamSafeArea so the card never slips under chrome.
|
||||
return (
|
||||
<View
|
||||
className="flex-1 items-center justify-center px-6"
|
||||
@@ -118,7 +153,7 @@ export function TextParticleView({
|
||||
showsVerticalScrollIndicator
|
||||
indicatorStyle="white"
|
||||
>
|
||||
<Text className="text-white text-lg leading-relaxed">{content}</Text>
|
||||
{markdownNodes}
|
||||
{editedLabel}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user