import { type Mention, commands } from "@/commands.gen"; import { cn, displayNpub } from "@/commons"; import { PublishIcon, Spinner } from "@/components"; import { Note } from "@/components/note"; import { User } from "@/components/user"; import { LumeWindow, useEvent } from "@/system"; import type { Metadata } from "@/types"; import { CaretDown } from "@phosphor-icons/react"; import { createLazyFileRoute, useAwaited } from "@tanstack/react-router"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { message } from "@tauri-apps/plugin-dialog"; import { useCallback, useEffect, useMemo, useRef, useState, useTransition, } from "react"; import { createPortal } from "react-dom"; import { RichTextarea, type RichTextareaHandle, createRegexRenderer, } from "rich-textarea"; import { MediaButton } from "./-components/media"; import { PowButton } from "./-components/pow"; import { WarningButton } from "./-components/warning"; const MENTION_REG = /\B@([\-+\w]*)$/; const MAX_LIST_LENGTH = 5; const renderer = createRegexRenderer([ [ /https?:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+/g, ({ children, key, value }) => ( {children} ), ], [ /(?:^|\W)nostr:(\w+)(?!\w)/g, ({ children, key }) => ( {children} ), ], [ /(?:^|\W)#(\w+)(?!\w)/g, ({ children, key }) => ( {children} ), ], ]); export const Route = createLazyFileRoute("/new-post/")({ component: Screen, }); function Screen() { const { reply_to } = Route.useSearch(); const { accounts, initialValue } = Route.useRouteContext(); const { deferMentionList } = Route.useLoaderData(); const users = useAwaited({ promise: deferMentionList })[0]; const [text, setText] = useState(""); const [currentUser, setCurrentUser] = useState(null); const [isPublish, setIsPublish] = useState(false); const [error, setError] = useState(""); const [isPending, startTransition] = useTransition(); const [warning, setWarning] = useState({ enable: false, reason: "" }); const [difficulty, setDifficulty] = useState({ enable: false, num: 21 }); const [index, setIndex] = useState(0); const [pos, setPos] = useState<{ top: number; left: number; caret: number; } | null>(null); const ref = useRef(null); const targetText = pos ? text.slice(0, pos.caret) : text; const match = pos && targetText.match(MENTION_REG); const name = match?.[1] ?? ""; const filtered = useMemo(() => { if (!users?.length) return []; return users .filter((u) => u?.name?.toLowerCase().startsWith(name.toLowerCase())) .slice(0, MAX_LIST_LENGTH); }, [users, name]); const showContextMenu = useCallback(async (e: React.MouseEvent) => { e.preventDefault(); const list: Promise[] = []; for (const account of accounts) { const res = await commands.getProfile(account); let name = "unknown"; if (res.status === "ok") { const profile: Metadata = JSON.parse(res.data); name = profile.display_name ?? profile.name ?? "anon"; } list.push( MenuItem.new({ text: `Publish as ${name} (${displayNpub(account, 16)})`, action: async () => setCurrentUser(account), }), ); } const items = await Promise.all(list); const menu = await Menu.new({ items }); await menu.popup().catch((e) => console.error(e)); }, []); const insert = (i: number) => { if (!ref.current || !pos) return; const selected = filtered[i]; ref.current.setRangeText( `nostr:${selected.pubkey} `, pos.caret - name.length - 1, pos.caret, "end", ); setPos(null); setIndex(0); }; const submit = () => { startTransition(async () => { if (!text.length) return; if (!currentUser) return; const signer = await commands.hasSigner(currentUser); if (signer.status === "ok") { if (!signer.data) { const res = await commands.setSigner(currentUser); if (res.status === "error") { await message(res.error, { kind: "error" }); return; } } const content = text.trim(); const warn = warning.enable ? warning.reason : null; const diff = difficulty.enable ? difficulty.num : null; if (reply_to?.length) { const res = await commands.reply(content, reply_to); if (res.status === "ok") { setText(""); setIsPublish(true); await getCurrentWindow().emit(reply_to, {}); } else { setError(res.error); } } else { const res = await commands.publish(content, warn, diff); if (res.status === "ok") { setText(""); setIsPublish(true); await LumeWindow.openColumn({ name: "Thread", label: res.data.slice(0, 6), account: currentUser, url: `/columns/events/${res.data}`, }); } else { setError(res.error); } } } }); }; useEffect(() => { if (isPublish) { const timer = setTimeout(() => setIsPublish((prev) => !prev), 3000); return () => { clearTimeout(timer); }; } }, [isPublish]); useEffect(() => { if (initialValue?.length) { setText(initialValue); } }, [initialValue]); useEffect(() => { if (accounts?.length) { setCurrentUser(accounts[0]); } }, [accounts]); return (
{error?.length ? (

Error: {error}

) : null} {reply_to?.length ? (
Reply to:
) : null}
setText(e.target.value)} onKeyDown={(e) => { if (!pos || !filtered.length) return; switch (e.code) { case "ArrowUp": { e.preventDefault(); const nextIndex = index <= 0 ? filtered.length - 1 : index - 1; setIndex(nextIndex); break; } case "ArrowDown": { e.preventDefault(); const prevIndex = index >= filtered.length - 1 ? 0 : index + 1; setIndex(prevIndex); break; } case "Enter": e.preventDefault(); insert(index); break; case "Escape": e.preventDefault(); setPos(null); setIndex(0); break; default: break; } }} onSelectionChange={(r) => { if ( r.focused && MENTION_REG.test(text.slice(0, r.selectionStart)) ) { setPos({ top: r.top + r.height, left: r.left, caret: r.selectionStart, }); setIndex(0); } else { setPos(null); setIndex(0); } }} disabled={isPending} > {renderer} {pos ? ( createPortal( , document.body, ) ) : ( <> )}
{warning.enable ? (
Reason: setWarning((prev) => ({ ...prev, reason: e.target.value })) } className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50" />
) : null} {difficulty.enable ? (
Difficulty: { if (!/[0-9]/.test(event.key)) { event.preventDefault(); } }} placeholder="21" defaultValue={difficulty.num} onChange={(e) => setWarning((prev) => ({ ...prev, num: Number(e.target.value) })) } className="flex-1 text-sm bg-transparent border-none focus:outline-none focus:ring-0 placeholder:text-black/50 dark:placeholder:text-white/50" />
) : null}
{currentUser ? ( ) : null}
); } function MentionPopup({ users, index, top, left, insert, }: { users: Mention[]; index: number; top: number; left: number; insert: (index: number) => void; }) { return (
{users.map((u, i) => (
{ e.preventDefault(); insert(i); }} >
{u.avatar?.length ? ( ) : (
)}
{u.name}
))}
); } function EmbedNote({ id }: { id: string }) { const { isLoading, isError, data } = useEvent(id); if (isLoading) { return ; } if (isError || !data) { return
Event not found with your current relay set.
; } return (
{data.content}
); }