Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
9.7 kB · 310 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311import Link from "next/link";import { useLeafletPublicationData } from "components/PageSWRDataProvider";import { useRef, useState } from "react";import { useReplicache } from "src/replicache";import { AsyncValueAutosizeTextarea } from "components/utils/AutosizeTextarea";import { Separator } from "components/Layout";import { AtUri } from "@atproto/syntax";import { getBasePublicationURL, getPublicationURL,} from "app/lish/createPub/getPublicationURL";import { useSubscribe } from "src/replicache/useSubscribe";import { useEntitySetContext } from "components/EntitySetProvider";import { timeAgo } from "src/utils/timeAgo";import { CommentTiny } from "components/Icons/CommentTiny";import { QuoteTiny } from "components/Icons/QuoteTiny";import { TagTiny } from "components/Icons/TagTiny";import { Popover } from "components/Popover";import { TagSelector } from "components/Tags";import { useIdentityData } from "components/IdentityProvider";import { PostHeaderLayout } from "app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader";import { Backdater } from "./Backdater";import { RecommendTinyEmpty } from "components/Icons/RecommendTiny";import { mergePreferences } from "src/utils/mergePreferences";
export const PublicationMetadata = (props: { noInteractions?: boolean }) => { let { rep } = useReplicache(); let { data: pub, normalizedDocument, normalizedPublication, } = useLeafletPublicationData(); let { identity } = useIdentityData(); let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title")); let description = useSubscribe(rep, (tx) => tx.get<string>("publication_description"), ); let postPreferences = useSubscribe(rep, (tx) => tx.get<{ showComments?: boolean; showMentions?: boolean; showRecommends?: boolean; } | null>("post_preferences"), ); let merged = mergePreferences( postPreferences || undefined, normalizedPublication?.preferences, ); let publishedAt = normalizedDocument?.publishedAt;
if (!pub) return null;
if (typeof title !== "string") { title = pub?.title || ""; } if (typeof description !== "string") { description = pub?.description || ""; } let tags = true;
return ( <PostHeaderLayout pubLink={ <div className="flex gap-2 items-center"> {pub.publications && ( <Link href={ identity?.atp_did === pub.publications?.identity_did ? `${getBasePublicationURL(pub.publications)}/dashboard` : getPublicationURL(pub.publications) } className="leafletMetadata text-accent-contrast font-bold hover:no-underline" > {pub.publications?.name} </Link> )} <div className="font-bold text-tertiary px-1 h-[20px] text-sm flex place-items-center bg-border-light rounded-md "> DRAFT </div> </div> } postTitle={ <TextField className="leading-tight pt-0.5 text-xl font-bold outline-hidden bg-transparent" value={title} onChange={async (newTitle) => { await rep?.mutate.updatePublicationDraft({ title: newTitle, description, }); }} placeholder="Untitled" /> } postDescription={ <TextField placeholder="add an optional description..." className="pt-1 italic text-secondary outline-hidden bg-transparent" value={description} onChange={async (newDescription) => { await rep?.mutate.updatePublicationDraft({ title, description: newDescription, }); }} /> } postInfo={ <> {pub.doc ? ( <div className="flex gap-2 items-center"> <p className="text-sm text-tertiary"> Published{" "} {publishedAt && ( <Backdater publishedAt={publishedAt} docURI={pub.doc} /> )} </p>
<Link target="_blank" className="text-sm" href={ pub.publications ? `${getPublicationURL(pub.publications)}/${new AtUri(pub.doc).rkey}` : `/p/${new AtUri(pub.doc).host}/${new AtUri(pub.doc).rkey}` } > View </Link> </div> ) : ( <p>Draft</p> )} {!props.noInteractions && ( <div className="flex gap-2 text-border items-center"> {merged.showRecommends !== false && ( <div className="flex gap-1 items-center"> <RecommendTinyEmpty />— </div> )}
{merged.showMentions !== false && ( <div className="flex gap-1 items-center"> <QuoteTiny />— </div> )} {merged.showComments !== false && ( <div className="flex gap-1 items-center"> <CommentTiny />— </div> )} {tags && ( <> {merged.showRecommends !== false || merged.showMentions !== false || merged.showComments !== false ? ( <Separator classname="h-4!" /> ) : null} <AddTags /> </> )} </div> )} </> } /> );};
export const TextField = ({ value, onChange, className, placeholder,}: { value: string; onChange: (v: string) => Promise<void>; className: string; placeholder: string;}) => { let { undoManager } = useReplicache(); let actionTimeout = useRef<number | null>(null); let { permissions } = useEntitySetContext(); let previousSelection = useRef<null | { start: number; end: number }>(null); let ref = useRef<HTMLTextAreaElement | null>(null); return ( <AsyncValueAutosizeTextarea ref={ref} disabled={!permissions.write} onSelect={(e) => { let start = e.currentTarget.selectionStart, end = e.currentTarget.selectionEnd; previousSelection.current = { start, end }; }} className={className} value={value} onBlur={async () => { if (actionTimeout.current) { undoManager.endGroup(); window.clearTimeout(actionTimeout.current); actionTimeout.current = null; } }} onChange={async (e) => { let newValue = e.currentTarget.value; let oldValue = value; let start = e.currentTarget.selectionStart, end = e.currentTarget.selectionEnd; await onChange(e.currentTarget.value);
if (actionTimeout.current) { window.clearTimeout(actionTimeout.current); } else { undoManager.startGroup(); }
actionTimeout.current = window.setTimeout(() => { undoManager.endGroup(); actionTimeout.current = null; }, 200); let previousStart = previousSelection.current?.start || null, previousEnd = previousSelection.current?.end || null; undoManager.add({ redo: async () => { await onChange(newValue); ref.current?.setSelectionRange(start, end); ref.current?.focus(); }, undo: async () => { await onChange(oldValue); ref.current?.setSelectionRange(previousStart, previousEnd); ref.current?.focus(); }, }); }} placeholder={placeholder} /> );};
export const PublicationMetadataPreview = () => { let { data: pub, normalizedDocument } = useLeafletPublicationData(); let publishedAt = normalizedDocument?.publishedAt;
if (!pub) return null;
return ( <PostHeaderLayout pubLink={ <div className="text-accent-contrast font-bold hover:no-underline"> {pub.publications?.name} </div> } postTitle={pub.title} postDescription={pub.description} postInfo={ pub.doc ? ( <p>Published {publishedAt && timeAgo(publishedAt)}</p> ) : ( <p>Draft</p> ) } /> );};
export const AddTags = () => { let { data: pub, normalizedDocument } = useLeafletPublicationData(); let { rep } = useReplicache();
// Get tags from Replicache local state or published document let replicacheTags = useSubscribe(rep, (tx) => tx.get<string[]>("publication_tags"), );
// Determine which tags to use - prioritize Replicache state let tags: string[] = []; if (Array.isArray(replicacheTags)) { tags = replicacheTags; } else if ( normalizedDocument?.tags && Array.isArray(normalizedDocument.tags) ) { tags = normalizedDocument.tags as string[]; }
// Update tags in replicache local state const handleTagsChange = async (newTags: string[]) => { // Store tags in replicache for next publish/update await rep?.mutate.updatePublicationDraft({ tags: newTags, }); };
return ( <Popover className="p-2! w-full min-w-xs" trigger={ <div className="addTagTrigger flex gap-1 hover:underline text-sm items-center text-tertiary"> <TagTiny />{" "} {tags.length > 0 ? `${tags.length} Tag${tags.length === 1 ? "" : "s"}` : "Add Tags"} </div> } > <TagSelector selectedTags={tags} setSelectedTags={handleTagsChange} /> </Popover> );};