a tool for shared writing and social publishing
1import Link from "next/link"; 2import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 3import { useRef } from "react"; 4import { useReplicache } from "src/replicache"; 5import { AsyncValueAutosizeTextarea } from "components/utils/AutosizeTextarea"; 6import { Separator } from "components/Layout"; 7import { AtUri } from "@atproto/syntax"; 8import { PubLeafletDocument } from "lexicons/api"; 9import { 10 getBasePublicationURL, 11 getPublicationURL, 12} from "app/lish/createPub/getPublicationURL"; 13import { useSubscribe } from "src/replicache/useSubscribe"; 14import { useEntitySetContext } from "components/EntitySetProvider"; 15import { timeAgo } from "src/utils/timeAgo"; 16import { useIdentityData } from "components/IdentityProvider"; 17export const PublicationMetadata = () => { 18 let { rep } = useReplicache(); 19 let { data: pub } = useLeafletPublicationData(); 20 let { identity } = useIdentityData(); 21 let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title")); 22 let description = useSubscribe(rep, (tx) => 23 tx.get<string>("publication_description"), 24 ); 25 let record = pub?.documents?.data as PubLeafletDocument.Record | null; 26 let publishedAt = record?.publishedAt; 27 28 if (!pub || !pub.publications) return null; 29 30 if (typeof title !== "string") { 31 title = pub?.title || ""; 32 } 33 if (typeof description !== "string") { 34 description = pub?.description || ""; 35 } 36 return ( 37 <div className={`flex flex-col px-3 sm:px-4 pb-5 sm:pt-3 pt-2`}> 38 <div className="flex gap-2"> 39 <Link 40 href={ 41 identity?.atp_did === pub.publications?.identity_did 42 ? `${getBasePublicationURL(pub.publications)}/dashboard` 43 : getPublicationURL(pub.publications) 44 } 45 className="leafletMetadata text-accent-contrast font-bold hover:no-underline" 46 > 47 {pub.publications?.name} 48 </Link> 49 <div className="font-bold text-tertiary px-1 text-sm flex place-items-center bg-border-light rounded-md "> 50 Editor 51 </div> 52 </div> 53 <TextField 54 className="text-xl font-bold outline-hidden bg-transparent" 55 value={title} 56 onChange={async (newTitle) => { 57 await rep?.mutate.updatePublicationDraft({ 58 title: newTitle, 59 description, 60 }); 61 }} 62 placeholder="Untitled" 63 /> 64 <TextField 65 placeholder="add an optional description..." 66 className="italic text-secondary outline-hidden bg-transparent" 67 value={description} 68 onChange={async (newDescription) => { 69 await rep?.mutate.updatePublicationDraft({ 70 title, 71 description: newDescription, 72 }); 73 }} 74 /> 75 {pub.doc ? ( 76 <div className="flex flex-row items-center gap-2 pt-3"> 77 <p className="text-sm text-tertiary"> 78 Published {publishedAt && timeAgo(publishedAt)} 79 </p> 80 <Separator classname="h-4" /> 81 <Link 82 target="_blank" 83 className="text-sm" 84 href={`${getPublicationURL(pub.publications)}/${new AtUri(pub.doc).rkey}`} 85 > 86 View Post 87 </Link> 88 </div> 89 ) : ( 90 <p className="text-sm text-tertiary pt-2">Draft</p> 91 )} 92 </div> 93 ); 94}; 95 96export const TextField = ({ 97 value, 98 onChange, 99 className, 100 placeholder, 101}: { 102 value: string; 103 onChange: (v: string) => Promise<void>; 104 className: string; 105 placeholder: string; 106}) => { 107 let { undoManager } = useReplicache(); 108 let actionTimeout = useRef<number | null>(null); 109 let { permissions } = useEntitySetContext(); 110 let previousSelection = useRef<null | { start: number; end: number }>(null); 111 let ref = useRef<HTMLTextAreaElement | null>(null); 112 return ( 113 <AsyncValueAutosizeTextarea 114 ref={ref} 115 disabled={!permissions.write} 116 onSelect={(e) => { 117 let start = e.currentTarget.selectionStart, 118 end = e.currentTarget.selectionEnd; 119 previousSelection.current = { start, end }; 120 }} 121 className={className} 122 value={value} 123 onBlur={async () => { 124 if (actionTimeout.current) { 125 undoManager.endGroup(); 126 window.clearTimeout(actionTimeout.current); 127 actionTimeout.current = null; 128 } 129 }} 130 onChange={async (e) => { 131 let newValue = e.currentTarget.value; 132 let oldValue = value; 133 let start = e.currentTarget.selectionStart, 134 end = e.currentTarget.selectionEnd; 135 await onChange(e.currentTarget.value); 136 137 if (actionTimeout.current) { 138 window.clearTimeout(actionTimeout.current); 139 } else { 140 undoManager.startGroup(); 141 } 142 143 actionTimeout.current = window.setTimeout(() => { 144 undoManager.endGroup(); 145 actionTimeout.current = null; 146 }, 200); 147 let previousStart = previousSelection.current?.start || null, 148 previousEnd = previousSelection.current?.end || null; 149 undoManager.add({ 150 redo: async () => { 151 await onChange(newValue); 152 ref.current?.setSelectionRange(start, end); 153 ref.current?.focus(); 154 }, 155 undo: async () => { 156 await onChange(oldValue); 157 ref.current?.setSelectionRange(previousStart, previousEnd); 158 ref.current?.focus(); 159 }, 160 }); 161 }} 162 placeholder={placeholder} 163 /> 164 ); 165}; 166 167export const PublicationMetadataPreview = () => { 168 let { data: pub } = useLeafletPublicationData(); 169 let record = pub?.documents?.data as PubLeafletDocument.Record | null; 170 let publishedAt = record?.publishedAt; 171 172 if (!pub || !pub.publications) return null; 173 174 return ( 175 <div className={`flex flex-col px-3 sm:px-4 pb-5 sm:pt-3 pt-2`}> 176 <div className="text-accent-contrast font-bold hover:no-underline"> 177 {pub.publications?.name} 178 </div> 179 180 <div 181 className={`text-xl font-bold outline-hidden bg-transparent ${!pub.title && "text-tertiary italic"}`} 182 > 183 {pub.title ? pub.title : "Untitled"} 184 </div> 185 <div className="italic text-secondary outline-hidden bg-transparent"> 186 {pub.description} 187 </div> 188 189 {pub.doc ? ( 190 <div className="flex flex-row items-center gap-2 pt-3"> 191 <p className="text-sm text-tertiary"> 192 Published {publishedAt && timeAgo(publishedAt)} 193 </p> 194 </div> 195 ) : ( 196 <p className="text-sm text-tertiary pt-2">Draft</p> 197 )} 198 </div> 199 ); 200};