import { AppBskyRichtextFacet, RichText } from "@atproto/api"; import { useAtom } from "jotai"; import { Dialog } from "radix-ui"; import { useEffect, useRef, useState } from "react"; import { useAuth } from "~/providers/UnifiedAuthProvider"; import { composerAtom } from "~/utils/atoms"; import { useQueryPost } from "~/utils/useQuery"; import { ProfileThing } from "./Login"; import { Button } from "./radix-m3-rd/Button"; import { UniversalPostRendererATURILoader } from "./UniversalPostRenderer"; const MAX_POST_LENGTH = 300; export function Composer() { const [composerState, setComposerState] = useAtom(composerAtom); const [closeConfirmState, setCloseConfirmState] = useState(false); const { agent } = useAuth(); const [postText, setPostText] = useState(""); const [posting, setPosting] = useState(false); const [postSuccess, setPostSuccess] = useState(false); const [postError, setPostError] = useState(null); useEffect(() => { setPostText(""); setPosting(false); setPostSuccess(false); setPostError(null); }, [composerState.kind]); const parentUri = composerState.kind === "reply" ? composerState.parent : composerState.kind === "quote" ? composerState.subject : undefined; const { data: parentPost, isLoading: isParentLoading } = useQueryPost(parentUri); async function handlePost() { if (!agent || !postText.trim() || postText.length > MAX_POST_LENGTH) return; setPosting(true); setPostError(null); try { const rt = new RichText({ text: postText }); await rt.detectFacets(agent); if (rt.facets?.length) { rt.facets = rt.facets.filter((item) => { if (item.$type !== "app.bsky.richtext.facet") return true; if (!item.features?.length) return true; item.features = item.features.filter((feature) => { if (feature.$type !== "app.bsky.richtext.facet#mention") return true; const did = feature.$type === "app.bsky.richtext.facet#mention" ? (feature as AppBskyRichtextFacet.Mention)?.did : undefined; return typeof did === "string" && did.startsWith("did:"); }); return item.features.length > 0; }); } const record: Record = { $type: "app.bsky.feed.post", text: rt.text, facets: rt.facets, createdAt: new Date().toISOString(), }; if (composerState.kind === "reply" && parentPost) { record.reply = { root: parentPost.value?.reply?.root ?? { uri: parentPost.uri, cid: parentPost.cid, }, parent: { uri: parentPost.uri, cid: parentPost.cid, }, }; } if (composerState.kind === "quote" && parentPost) { record.embed = { $type: "app.bsky.embed.record", record: { uri: parentPost.uri, cid: parentPost.cid, }, }; } await agent.com.atproto.repo.createRecord({ collection: "app.bsky.feed.post", repo: agent.assertDid, record, }); setPostSuccess(true); setPostText(""); setTimeout(() => { setPostSuccess(false); setComposerState({ kind: "closed" }); }, 1500); } catch (e: any) { setPostError(e?.message || "Failed to post"); } finally { setPosting(false); } } const getPlaceholder = () => { switch (composerState.kind) { case "reply": return "Post your reply"; case "quote": return "Add a comment..."; case "root": default: return "What's happening?!"; } }; const charsLeft = MAX_POST_LENGTH - postText.length; const isPostButtonDisabled = posting || !postText.trim() || isParentLoading || charsLeft < 0; function handleAttemptClose() { if (postText.trim() && !posting) { setCloseConfirmState(true); } else { setComposerState({ kind: "closed" }); } } function handleConfirmClose() { setComposerState({ kind: "closed" }); setCloseConfirmState(false); setPostText(""); } return ( <> { if (!open) handleAttemptClose(); }} >
{charsLeft}
{postSuccess ? (
✓ Posted!
) : (
{composerState.kind === "reply" && (
{isParentLoading ? (
Loading parent post...
) : parentUri ? ( ) : (
Could not load parent post.
)}
)}
setPostText(e.target.value)} disabled={posting} autoFocus />
{composerState.kind === "quote" && (
{isParentLoading ? (
Loading parent post...
) : parentUri ? ( ) : (
Could not load parent post.
)}
)} {postError && (
{postError}
)}
)}
{/* Close confirmation dialog */}
Discard your post?
You will lose your draft
); } function AutoGrowTextarea({ value, className, onChange, ...props }: React.DetailedHTMLProps< React.TextareaHTMLAttributes, HTMLTextAreaElement >) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; el.style.height = "auto"; el.style.height = el.scrollHeight + "px"; }, [value]); return (