summaryrefslogtreecommitdiff
path: root/front/src/components/composer
diff options
context:
space:
mode:
Diffstat (limited to 'front/src/components/composer')
-rw-r--r--front/src/components/composer/Composer.tsx198
-rw-r--r--front/src/components/composer/Snippets.tsx82
2 files changed, 206 insertions, 74 deletions
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<HTMLInputElement>(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<HTMLFormElement>) {
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 (
- <form id="composer" onSubmit={poast}>
+ <form
+ id="composer"
+ className={`${isExpanded ? "expanded" : ""} ${composerData ? "has-context" : ""}`}
+ onSubmit={poast}
+ >
<div className="sigil avatar">
<Sigil patp={our} size={46} />
</div>
- {composerData && composerData.type === "reply" && (
- <ReplySnippet post={composerData?.post} />
- )}
- <input
- value={input}
- onInput={(e) => setInput(e.currentTarget.value)}
- placeholder={placeHolder}
- />
- {composerData && composerData.type === "quote" && (
- <Snippets post={composerData?.post} />
- )}
- <button type="submit">Post</button>
+ <div className="composer-content">
+ {/* Reply snippets appear above input */}
+ {composerData && composerData.type === "reply" && (
+ <div className="composer-context reply-context">
+ <div className="context-header">
+ <span className="context-type">
+ <Icon name="reply" size={14} /> Replying to
+ </span>
+ <button
+ className="clear-context"
+ onClick={clearComposer}
+ title="Clear"
+ type="button"
+ >
+ ×
+ </button>
+ </div>
+ <ReplySnippet post={composerData.post} />
+ </div>
+ )}
+
+ {/* Quote context header above input (without snippet) */}
+ {composerData && composerData.type === "quote" && (
+ <div className="quote-header">
+ <div className="context-header">
+ <span className="context-type">
+ <Icon name="quote" size={14} /> Quote posting
+ </span>
+ <button
+ className="clear-context"
+ onClick={clearComposer}
+ title="Clear"
+ type="button"
+ >
+ ×
+ </button>
+ </div>
+ </div>
+ )}
+
+ <div className="composer-input-row">
+ <input
+ ref={inputRef}
+ value={input}
+ onInput={(e) => setInput(e.currentTarget.value)}
+ onFocus={() => setIsExpanded(true)}
+ placeholder={placeHolder}
+ />
+ <button type="submit" disabled={!input.trim()} className="post-btn">
+ Post
+ </button>
+ </div>
+
+ {/* Quote snippets appear below input */}
+ {composerData && composerData.type === "quote" && (
+ <div className="composer-context quote-context">
+ <Snippets post={composerData.post} />
+ </div>
+ )}
+ </div>
</form>
);
}
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 (
<div className="composer-snippet">
- <div className="pop-snippet-icon cp" role="link" onClick={onc}></div>
+ {onClick && (
+ <div className="pop-snippet-icon cp" role="link" onClick={onc}>
+ ×
+ </div>
+ )}
{children}
</div>
);
}
function PostSnippet({ post }: { post: SPID }) {
- if ("trill" in post) return <Quote data={post.trill} nest={0} />;
- else if ("nostr" in post) return <NostrSnippet {...post.nostr} />;
- // else if ("twatter" in post)
- // return (
- // <div id={`composer-${type}`}>
- // <Tweet tweet={post.post} quote={true} />
- // </div>
- // );
- // else if ("rumors" in post)
- // return (
- // <div id={`composer-${type}`}>
- // <div className="rumor-quote f1">
- // <img src={rumorIcon} alt="" />
- // <Body poast={post.post} refetch={() => {}} />
- // <span>{date_diff(post.post.time, "short")}</span>
- // </div>
- // </div>
- // );
- else return <></>;
+ if (!post) return <div className="snippet-error">No post data</div>;
+
+ try {
+ if ("trill" in post) return <Quote data={post.trill} nest={0} />;
+ else if ("nostr" in post) return <NostrSnippet {...post.nostr} />;
+ // else if ("twatter" in post)
+ // return (
+ // <div id={`composer-${type}`}>
+ // <Tweet tweet={post.post} quote={true} />
+ // </div>
+ // );
+ // else if ("rumors" in post)
+ // return (
+ // <div id={`composer-${type}`}>
+ // <div className="rumor-quote f1">
+ // <img src={rumorIcon} alt="" />
+ // <Body poast={post.post} refetch={() => {}} />
+ // <span>{date_diff(post.post.time, "short")}</span>
+ // </div>
+ // </div>
+ // );
+ else return <div className="snippet-error">Unsupported post type</div>;
+ } catch (error) {
+ console.error("Error rendering post snippet:", error);
+ return <div className="snippet-error">Failed to load post</div>;
+ }
}
export function ReplySnippet({ post }: { post: SPID }) {
- if ("trill" in post)
- return (
- <div id="reply">
- <Quote data={post.trill} nest={0} />
- </div>
- );
- else return <div />;
+ if (!post) return <div className="snippet-error">No post to reply to</div>;
+
+ try {
+ if ("trill" in post)
+ return (
+ <div id="reply" className="reply-snippet">
+ <Quote data={post.trill} nest={0} />
+ </div>
+ );
+ else if ("nostr" in post)
+ return (
+ <div id="reply" className="reply-snippet">
+ <NostrSnippet {...post.nostr} />
+ </div>
+ );
+ else return <div className="snippet-error">Cannot reply to this post type</div>;
+ } catch (error) {
+ console.error("Error rendering reply snippet:", error);
+ return <div className="snippet-error">Failed to load reply context</div>;
+ }
}