From 7bac4927e8895719a91011da9a2b997579238145 Mon Sep 17 00:00:00 2001 From: polwex Date: Thu, 18 Sep 2025 08:26:30 +0700 Subject: damn my trill codebase was really something --- front/src/components/composer/Composer.tsx | 198 ++++++++++++++++++++++------- front/src/components/composer/Snippets.tsx | 82 +++++++----- 2 files changed, 206 insertions(+), 74 deletions(-) (limited to 'front/src/components/composer') diff --git a/front/src/components/composer/Composer.tsx b/front/src/components/composer/Composer.tsx index 43d38cd..81d0358 100644 --- a/front/src/components/composer/Composer.tsx +++ b/front/src/components/composer/Composer.tsx @@ -1,28 +1,44 @@ import useLocalState from "@/state/state"; import type { Poast } from "@/types/trill"; import Sigil from "@/components/Sigil"; -import { useState, type FormEvent } from "react"; -import type { ComposerData } from "@/types/ui"; +import { useState, useEffect, useRef, type FormEvent } from "react"; import Snippets, { ReplySnippet } from "./Snippets"; import toast from "react-hot-toast"; -import { useLocation } from "wouter"; +import Icon from "@/components/Icon"; +import { wait } from "@/logic/utils"; -function Composer({ - isAnon, - replying, -}: { - isAnon?: boolean; - replying?: Poast; -}) { - const [loc, navigate] = useLocation(); - const { api, composerData, addNotification, setComposerData } = useLocalState((s) => ({ - api: s.api, - composerData: s.composerData, - addNotification: s.addNotification, - setComposerData: s.setComposerData, - })); +function Composer({ isAnon }: { isAnon?: boolean }) { + const { api, composerData, addNotification, setComposerData } = useLocalState( + (s) => ({ + api: s.api, + composerData: s.composerData, + addNotification: s.addNotification, + setComposerData: s.setComposerData, + }), + ); const our = api!.airlock.our!; - const [input, setInput] = useState(replying ? `${replying}: ` : ""); + const [input, setInput] = useState(""); + const [isExpanded, setIsExpanded] = useState(false); + const [isLoading, setLoading] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + if (composerData) { + setIsExpanded(true); + if ( + composerData.type === "reply" && + composerData.post && + "trill" in composerData.post + ) { + const author = composerData.post.trill.author; + setInput(`${author} `); + } + // Auto-focus input when composer opens + setTimeout(() => { + inputRef.current?.focus(); + }, 100); // Small delay to ensure the composer is rendered + } + }, [composerData]); async function poast(e: FormEvent) { e.preventDefault(); // TODO @@ -39,13 +55,32 @@ function Composer({ // tags: input.match(HASHTAGS_REGEX) || [], // }; // TODO make it user choosable - const res = await api!.addPost(input); - if (res) { - // Check for mentions in the post (ship names starting with ~) + setLoading(true); + + const res = + composerData?.type === "reply" && "trill" in composerData.post + ? api!.addReply( + input, + composerData.post.trill.host, + composerData.post.trill.id, + composerData.post.trill.thread || composerData.post.trill.id, + ) + : composerData?.type === "quote" && "trill" in composerData.post + ? api!.addQuote(input, { + ship: composerData.post.trill.host, + id: composerData.post.trill.id, + }) + : !composerData + ? api!.addPost(input) + : wait(500); + const ares = await res; + if (ares) { + // // Check for mentions in the post (ship names starting with ~) const mentions = input.match(/~[a-z-]+/g); if (mentions) { - mentions.forEach(mention => { - if (mention !== our) { // Don't notify self-mentions + mentions.forEach((mention) => { + if (mention !== our) { + // Don't notify self-mentions addNotification({ type: "mention", from: our, @@ -56,40 +91,113 @@ function Composer({ } // If this is a reply, add notification - if (composerData?.type === "reply" && composerData.post?.trill?.author !== our) { - addNotification({ - type: "reply", - from: our, - message: `You replied to ${composerData.post.trill.author}'s post`, - postId: composerData.post.trill.id, - }); + if ( + composerData?.type === "reply" && + composerData.post && + "trill" in composerData.post + ) { + if (composerData.post.trill.author !== our) { + addNotification({ + type: "reply", + from: our, + message: `You replied to ${composerData.post.trill.author}'s post`, + postId: composerData.post.trill.id, + }); + } } setInput(""); setComposerData(null); // Clear composer data after successful post toast.success("post sent"); - navigate(`/feed/${our}`); + setIsExpanded(false); } } - const placeHolder = isAnon ? "> be me" : "What's going on in Urbit"; + const placeHolder = + composerData?.type === "reply" + ? "Write your reply..." + : composerData?.type === "quote" + ? "Add your thoughts..." + : isAnon + ? "> be me" + : "What's going on in Urbit"; + + const clearComposer = (e: React.MouseEvent) => { + e.preventDefault(); + setComposerData(null); + setInput(""); + setIsExpanded(false); + }; + return ( -
+
- {composerData && composerData.type === "reply" && ( - - )} - setInput(e.currentTarget.value)} - placeholder={placeHolder} - /> - {composerData && composerData.type === "quote" && ( - - )} - +
+ {/* Reply snippets appear above input */} + {composerData && composerData.type === "reply" && ( +
+
+ + Replying to + + +
+ +
+ )} + + {/* Quote context header above input (without snippet) */} + {composerData && composerData.type === "quote" && ( +
+
+ + Quote posting + + +
+
+ )} + +
+ setInput(e.currentTarget.value)} + onFocus={() => setIsExpanded(true)} + placeholder={placeHolder} + /> + +
+ + {/* Quote snippets appear below input */} + {composerData && composerData.type === "quote" && ( +
+ +
+ )} +
); } diff --git a/front/src/components/composer/Snippets.tsx b/front/src/components/composer/Snippets.tsx index 30498d0..49d9b88 100644 --- a/front/src/components/composer/Snippets.tsx +++ b/front/src/components/composer/Snippets.tsx @@ -1,5 +1,5 @@ import Quote from "@/components/post/Quote"; -import type { ComposerData, SPID } from "@/types/ui"; +import type { SPID } from "@/types/ui"; import { NostrSnippet } from "../post/wrappers/Nostr"; export default Snippets; @@ -20,43 +20,67 @@ export function ComposerSnippet({ }) { function onc(e: React.MouseEvent) { e.stopPropagation(); - onClick(); + if (onClick) onClick(); } return (
-
+ {onClick && ( +
+ × +
+ )} {children}
); } function PostSnippet({ post }: { post: SPID }) { - if ("trill" in post) return ; - else if ("nostr" in post) return ; - // else if ("twatter" in post) - // return ( - //
- // - //
- // ); - // else if ("rumors" in post) - // return ( - //
- //
- // - // {}} /> - // {date_diff(post.post.time, "short")} - //
- //
- // ); - else return <>; + if (!post) return
No post data
; + + try { + if ("trill" in post) return ; + else if ("nostr" in post) return ; + // else if ("twatter" in post) + // return ( + //
+ // + //
+ // ); + // else if ("rumors" in post) + // return ( + //
+ //
+ // + // {}} /> + // {date_diff(post.post.time, "short")} + //
+ //
+ // ); + else return
Unsupported post type
; + } catch (error) { + console.error("Error rendering post snippet:", error); + return
Failed to load post
; + } } export function ReplySnippet({ post }: { post: SPID }) { - if ("trill" in post) - return ( -
- -
- ); - else return
; + if (!post) return
No post to reply to
; + + try { + if ("trill" in post) + return ( +
+ +
+ ); + else if ("nostr" in post) + return ( +
+ +
+ ); + else return
Cannot reply to this post type
; + } catch (error) { + console.error("Error rendering reply snippet:", error); + return
Failed to load reply context
; + } } -- cgit v1.2.3