a tool for shared writing and social publishing
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add update pub form

+219 -5
+2 -1
app/[leaflet_id]/Actions.tsx
··· 40 40 }; 41 41 42 42 export const PublishButton = () => { 43 - let { data } = useLeafletPublicationData(); 43 + let { data, mutate } = useLeafletPublicationData(); 44 44 let identity = useIdentityData(); 45 45 let { permission_token, rootEntity } = useReplicache(); 46 46 let rootPage = useEntity(rootEntity, "root/page")[0]; ··· 62 62 title: pub.title, 63 63 description: pub.description, 64 64 }); 65 + mutate(); 65 66 toaster({ 66 67 content: ( 67 68 <div>
+25 -3
app/lish/[did]/[publication]/dashboard/Actions.tsx
··· 12 12 import { MenuItem } from "components/Layout"; 13 13 import Link from "next/link"; 14 14 import { HomeSmall } from "components/Icons/HomeSmall"; 15 + import { EditPubForm } from "app/lish/createPub/UpdatePubForm"; 16 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 17 + import { usePublicationData } from "./PublicationSWRProvider"; 18 + import { useSmoker } from "components/Toast"; 15 19 16 20 export const Actions = (props: { publication: string }) => { 17 21 return ( ··· 43 47 }; 44 48 45 49 function PublicationShareButton() { 50 + let pub = usePublicationData(); 51 + let smoker = useSmoker(); 46 52 return ( 47 53 <Menu 48 54 className="max-w-xs" ··· 58 64 } 59 65 > 60 66 <MenuItem onSelect={() => {}}> 61 - <Link href={"/"} className="text-secondary hover:no-underline"> 67 + <Link 68 + href={getPublicationURL(pub!)} 69 + className="text-secondary hover:no-underline" 70 + > 62 71 <div>Viewer Mode</div> 63 72 <div className="font-normal text-tertiary text-sm"> 64 73 View your publication as a reader 65 74 </div> 66 75 </Link> 67 76 </MenuItem> 68 - <MenuItem onSelect={() => {}}> 77 + <MenuItem 78 + onSelect={(e) => { 79 + e.preventDefault(); 80 + let rect = (e.currentTarget as Element)?.getBoundingClientRect(); 81 + navigator.clipboard.writeText(getPublicationURL(pub!)); 82 + smoker({ 83 + position: { 84 + x: rect ? rect.left + (rect.right - rect.left) / 2 : 0, 85 + y: rect ? rect.top + 26 : 0, 86 + }, 87 + text: "Copied Publicaiton URL!", 88 + }); 89 + }} 90 + > 69 91 <div> 70 92 <div>Share Your Publication</div> 71 93 <div className="font-normal text-tertiary text-sm"> ··· 91 113 /> 92 114 } 93 115 > 94 - <CreatePubForm /> 116 + <EditPubForm /> 95 117 </Popover> 96 118 ); 97 119 }
+112
app/lish/createPub/UpdatePubForm.tsx
··· 1 + "use client"; 2 + import { callRPC } from "app/api/rpc/client"; 3 + import { createPublication } from "./createPublication"; 4 + import { ButtonPrimary } from "components/Buttons"; 5 + import { AddSmall } from "components/Icons/AddSmall"; 6 + import { Input, InputWithLabel } from "components/Input"; 7 + import { useRouter } from "next/navigation"; 8 + import { useState, useRef, useEffect } from "react"; 9 + import { getPublicationURL } from "./getPublicationURL"; 10 + import { updatePublication } from "./updatePublication"; 11 + import { usePublicationData } from "../[did]/[publication]/dashboard/PublicationSWRProvider"; 12 + import { PubLeafletPublication } from "lexicons/api"; 13 + import { mutate } from "swr"; 14 + 15 + export const EditPubForm = () => { 16 + let pubData = usePublicationData(); 17 + let [nameValue, setNameValue] = useState(""); 18 + let [descriptionValue, setDescriptionValue] = useState(""); 19 + let [logoFile, setLogoFile] = useState<File | null>(null); 20 + let [logoPreview, setLogoPreview] = useState<string | null>(null); 21 + let fileInputRef = useRef<HTMLInputElement>(null); 22 + useEffect(() => { 23 + if (!pubData || !pubData.record) return; 24 + let record = pubData.record as PubLeafletPublication.Record; 25 + setNameValue(record.name); 26 + setDescriptionValue(record.description || ""); 27 + if (record.icon) 28 + setLogoPreview( 29 + `url(https://bsky.social/xrpc/com.atproto.sync.getBlob?did=${pubData.identity_did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]})`, 30 + ); 31 + }, [pubData]); 32 + 33 + let router = useRouter(); 34 + return ( 35 + <form 36 + className="flex flex-col gap-3" 37 + onSubmit={async (e) => { 38 + if (!pubData) return; 39 + e.preventDefault(); 40 + let data = await updatePublication({ 41 + uri: pubData.uri, 42 + name: nameValue, 43 + description: descriptionValue, 44 + iconFile: logoFile, 45 + }); 46 + mutate("publication-data"); 47 + }} 48 + > 49 + <div className="flex flex-col items-center mb-4 gap-2"> 50 + <div className="text-center text-secondary flex flex-col "> 51 + <h3 className="-mb-1">Logo</h3> 52 + <p className="italic text-tertiary">(optional)</p> 53 + </div> 54 + <div 55 + className="w-24 h-24 rounded-full border-2 border-dotted border-accent-1 flex items-center justify-center cursor-pointer hover:border-accent-contrast" 56 + onClick={() => fileInputRef.current?.click()} 57 + > 58 + {logoPreview ? ( 59 + <img 60 + src={logoPreview} 61 + alt="Logo preview" 62 + className="w-full h-full rounded-full object-cover" 63 + /> 64 + ) : ( 65 + <AddSmall className="text-accent-1" /> 66 + )} 67 + </div> 68 + <input 69 + type="file" 70 + accept="image/*" 71 + className="hidden" 72 + ref={fileInputRef} 73 + onChange={(e) => { 74 + const file = e.target.files?.[0]; 75 + if (file) { 76 + setLogoFile(file); 77 + const reader = new FileReader(); 78 + reader.onload = (e) => { 79 + setLogoPreview(e.target?.result as string); 80 + }; 81 + reader.readAsDataURL(file); 82 + } 83 + }} 84 + /> 85 + </div> 86 + <InputWithLabel 87 + type="text" 88 + id="pubName" 89 + label="Publication Name" 90 + value={nameValue} 91 + onChange={(e) => { 92 + setNameValue(e.currentTarget.value); 93 + }} 94 + /> 95 + 96 + <InputWithLabel 97 + label="Description (optional)" 98 + textarea 99 + rows={3} 100 + id="pubDescription" 101 + value={descriptionValue} 102 + onChange={(e) => { 103 + setDescriptionValue(e.currentTarget.value); 104 + }} 105 + /> 106 + 107 + <div className="flex w-full justify-center"> 108 + <ButtonPrimary type="submit">Update Publication!</ButtonPrimary> 109 + </div> 110 + </form> 111 + ); 112 + };
+1 -1
app/lish/createPub/getPublicationURL.ts
··· 10 10 }) { 11 11 let record = pub.record as PubLeafletPublication.Record; 12 12 if (isProductionDomain() && record?.base_path) { 13 - return new URL(record.base_path); 13 + return record.base_path; 14 14 } 15 15 let aturi = new AtUri(pub.uri); 16 16 return `/lish/${aturi.host}/${record?.name || pub.name}`;
+79
app/lish/createPub/updatePublication.ts
··· 1 + "use server"; 2 + import { TID } from "@atproto/common"; 3 + import { AtpBaseClient, PubLeafletPublication } from "lexicons/api"; 4 + import { createOauthClient } from "src/atproto-oauth"; 5 + import { getIdentityData } from "actions/getIdentityData"; 6 + import { supabaseServerClient } from "supabase/serverClient"; 7 + import { Json } from "supabase/database.types"; 8 + 9 + export async function updatePublication({ 10 + uri, 11 + name, 12 + description, 13 + iconFile, 14 + }: { 15 + uri: string; 16 + name: string; 17 + description: string; 18 + iconFile: File | null; 19 + }) { 20 + const oauthClient = await createOauthClient(); 21 + let identity = await getIdentityData(); 22 + if (!identity || !identity.atp_did) return; 23 + 24 + let credentialSession = await oauthClient.restore(identity.atp_did); 25 + let agent = new AtpBaseClient( 26 + credentialSession.fetchHandler.bind(credentialSession), 27 + ); 28 + let { data: existingPub } = await supabaseServerClient 29 + .from("publications") 30 + .select("*") 31 + .eq("uri", uri) 32 + .single(); 33 + if (!existingPub || existingPub.identity_did! === identity.atp_did) return; 34 + 35 + let record: PubLeafletPublication.Record = { 36 + $type: "pub.leaflet.publication", 37 + name, 38 + ...(existingPub.record as object), 39 + }; 40 + 41 + if (description) { 42 + record.description = description; 43 + } 44 + 45 + // Upload the icon if provided How do I tell if there isn't a new one? 46 + if (iconFile && iconFile.size > 0) { 47 + const buffer = await iconFile.arrayBuffer(); 48 + const uploadResult = await agent.com.atproto.repo.uploadBlob( 49 + new Uint8Array(buffer), 50 + { encoding: iconFile.type }, 51 + ); 52 + 53 + if (uploadResult.data.blob) { 54 + record.icon = uploadResult.data.blob; 55 + } 56 + } 57 + 58 + let result = await agent.com.atproto.repo.putRecord({ 59 + repo: credentialSession.did!, 60 + rkey: TID.nextStr(), 61 + record, 62 + collection: record.$type, 63 + validate: false, 64 + }); 65 + 66 + //optimistically write to our db! 67 + let { data: publication } = await supabaseServerClient 68 + .from("publications") 69 + .update({ 70 + identity_did: credentialSession.did!, 71 + name: record.name, 72 + record: record as Json, 73 + }) 74 + .eq("uri", uri) 75 + .select() 76 + .single(); 77 + 78 + return { success: true, publication }; 79 + }