a tool for shared writing and social publishing
0

Configure Feed

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

bug fixes

celine (Mar 25, 2026, 4:40 PM EDT) e507a306 1fdf4258

+163 -132
+1
components/Domains/AddDomainForm.tsx
··· 24 24 <form 25 25 className="flex flex-col gap-1 max-w-full" 26 26 onSubmit={async (e) => { 27 + e.preventDefault(); 27 28 setLoading(true); 28 29 let { error } = await addDomain(value); 29 30 if (error) {
+153 -125
components/Domains/PublicationDomains.tsx
··· 14 14 } from "app/lish/[did]/[publication]/dashboard/PublicationSWRProvider"; 15 15 import { updatePublicationBasePath } from "app/lish/createPub/updatePublication"; 16 16 import { assignDomainToPublication } from "actions/domains/assignDomainToPublication"; 17 + import { removeDomainAssignment } from "actions/domains/removeDomainAssignment"; 17 18 import { PubSettingsHeader } from "app/lish/[did]/[publication]/dashboard/settings/PublicationSettings"; 18 19 import { AddDomainForm } from "./AddDomainForm"; 19 20 import { DomainSettingsView } from "./DomainSettingsView"; 20 21 import { PinTiny } from "components/Icons/PinTiny"; 21 22 import { LoadingTiny } from "components/Icons/LoadingTiny"; 22 23 import { AddTiny } from "components/Icons/AddTiny"; 24 + import { UnlinkTiny } from "components/Icons/UnlinkTiny"; 23 25 import { DotLoader } from "components/utils/DotLoader"; 24 26 import { useToaster } from "components/Toast"; 25 27 import type { CustomDomain } from "./DomainList"; 26 - 27 - type State = 28 - | "list" 29 - | "add-domain" 30 - | { type: "domain-settings"; domain: string }; 31 28 32 29 export function PublicationDomains(props: { 33 30 backToMenu: () => void; 34 31 publication_uri: string; 35 32 }) { 36 - let [state, setState] = useState<State>("list"); 37 - let [loading, setLoading] = useState(false); 38 33 let { data, mutate: mutatePubData } = usePublicationData(); 39 - let toaster = useToaster(); 40 34 let { publication: pubData } = data || {}; 41 35 let record = useNormalizedPublicationRecord(); 42 36 let { identity, mutate: mutateIdentity } = useIdentityData(); 37 + let toaster = useToaster(); 43 38 let basePath = record?.url?.replace(/^https?:\/\//, "") || ""; 44 - 45 - if (state === "add-domain") { 46 - return ( 47 - <div> 48 - <PubSettingsHeader backToMenuAction={() => setState("list")}> 49 - Add Domain 50 - </PubSettingsHeader> 51 - <AddDomainForm 52 - onDomainAdded={async (domain) => { 53 - await assignDomainToPublication({ 54 - domain, 55 - publication_uri: props.publication_uri, 56 - }); 57 - mutateIdentity(); 58 - mutate("publication-data"); 59 - setState({ type: "domain-settings", domain }); 60 - }} 61 - onBack={() => setState("list")} 62 - /> 63 - </div> 64 - ); 65 - } 66 - 67 - if (typeof state === "object" && state.type === "domain-settings") { 68 - return ( 69 - <div> 70 - <PubSettingsHeader backToMenuAction={() => setState("list")}> 71 - Domain Settings 72 - </PubSettingsHeader> 73 - <DomainSettingsView 74 - domain={state.domain} 75 - onBack={() => setState("list")} 76 - onRemoveAssignment={() => { 77 - mutate("publication-data"); 78 - setState("list"); 79 - }} 80 - onDeleteDomain={() => { 81 - mutate("publication-data"); 82 - setState("list"); 83 - }} 84 - /> 85 - </div> 86 - ); 87 - } 88 39 89 40 let pubDomains = pubData?.publication_domains || []; 90 41 let pubDomainNames = new Set(pubDomains.map((d) => d.domain)); ··· 95 46 Domains 96 47 </PubSettingsHeader> 97 48 <div className="flex flex-col gap-1 py-1"> 98 - <h4 className="text-sm">This Publication's Domains</h4> 49 + <h4 className="">This Publication's Domains</h4> 99 50 <div className="text-xs text-tertiary -mb-1 ">DEFAULT</div> 100 51 {pubDomains 101 52 .filter((d) => d.domain === basePath) ··· 106 57 publication_uri={props.publication_uri} 107 58 basePath={basePath} 108 59 mutatePubData={mutatePubData} 109 - onSettings={() => 110 - setState({ type: "domain-settings", domain: d.domain }) 111 - } 60 + mutateIdentity={mutateIdentity} 61 + toaster={toaster} 62 + onSettings={() => {}} 112 63 /> 113 64 ))} 114 65 {pubDomains.filter((d) => d.domain !== basePath).length !== 0 && ( ··· 123 74 publication_uri={props.publication_uri} 124 75 basePath={basePath} 125 76 mutatePubData={mutatePubData} 126 - onSettings={() => 127 - setState({ type: "domain-settings", domain: d.domain }) 128 - } 77 + mutateIdentity={mutateIdentity} 78 + toaster={toaster} 79 + onSettings={() => {}} 129 80 /> 130 81 ))} 131 82 </> 132 83 )} 84 + 133 85 <hr className="my-2 border-border-light" /> 134 - <h4 className="text-sm">Available Domains</h4> 86 + <h4 className="">Available Domains</h4> 135 87 {(() => { 136 88 let availableDomains = (identity?.custom_domains || []) 137 89 .filter((d) => !pubDomainNames.has(d.domain)) ··· 147 99 key={d.domain} 148 100 domainData={d} 149 101 publication_uri={props.publication_uri} 102 + mutatePubData={mutatePubData} 150 103 onAssigned={() => { 151 104 mutateIdentity(); 152 105 mutate("publication-data"); 153 106 }} 154 - onSettings={() => 155 - setState({ type: "domain-settings", domain: d.domain }) 156 - } 107 + onSettings={() => {}} 157 108 /> 158 109 ))} 159 110 <div className="text-sm text-tertiary pt-0.5"> ··· 178 129 publication_uri: string; 179 130 basePath: string; 180 131 mutatePubData: ReturnType<typeof usePublicationData>["mutate"]; 132 + mutateIdentity: ReturnType<typeof useIdentityData>["mutate"]; 133 + toaster: ReturnType<typeof useToaster>; 181 134 onSettings: () => void; 182 135 }) { 183 136 let { pending } = useDomainStatus(props.domain); 184 137 let [loading, setLoading] = useState(false); 185 - let toaster = useToaster(); 138 + let [unlinking, setUnlinking] = useState(false); 139 + let toaster = props.toaster; 186 140 187 141 return ( 188 142 <div className="text-sm text-secondary relative w-full flex items-center justify-between px-[6px] py-1 border rounded-md border-border-light"> ··· 205 159 </p> 206 160 <LoadingTiny className="animate-spin text-accent-contrast group-hover/pending:text-accent-2" /> 207 161 </button> 208 - ) : props.basePath === props.domain ? null : ( 209 - <button 210 - type="button" 211 - disabled={loading} 212 - onClick={async () => { 213 - setLoading(true); 214 - // Optimistically update the record's url so the UI 215 - // immediately reflects the new default domain 216 - props.mutatePubData( 217 - (current) => { 218 - if (!current) return current; 219 - let pub = current.publication; 220 - if (!pub?.record) return current; 221 - let rec = pub.record as Record<string, unknown>; 222 - if (typeof rec.url !== "string") return current; 223 - let protocol = 224 - rec.url.match(/^https?:\/\//)?.[0] || "https://"; 225 - return { 226 - ...current, 227 - publication: { 228 - ...pub, 229 - record: { ...rec, url: protocol + props.domain }, 230 - }, 231 - }; 232 - }, 233 - { revalidate: false }, 234 - ); 235 - toaster({ 236 - content: ( 237 - <div> 238 - Default domain set to <strong>{props.domain}</strong> 239 - </div> 240 - ), 241 - type: "success", 242 - }); 243 - await updatePublicationBasePath({ 244 - uri: props.publication_uri, 245 - base_path: props.domain, 246 - }); 247 - props.mutatePubData(); 248 - setLoading(false); 249 - }} 250 - className="group/domain flex gap-1 items-center rounded-full bg-none w-max font-bold px-1 py-0 hover:bg-accent-1 hover:text-accent-2 border-transparent outline-solid outline-transparent hover:outline-accent-1 selected-outline" 251 - > 252 - {loading ? ( 253 - <DotLoader className="h-[18px]! text-xs" /> 254 - ) : ( 255 - <> 256 - <p className="group-hover/domain:block hidden w-max pl-1 text-xs"> 257 - set as default 258 - </p> 259 - <PinTiny className="text-tertiary group-hover/domain:text-accent-2 shrink-0" /> 260 - </> 162 + ) : ( 163 + <> 164 + {props.basePath !== props.domain && ( 165 + <div className="flex gap-1"> 166 + {!props.domain.endsWith(".leaflet.pub") && ( 167 + <button 168 + type="button" 169 + disabled={unlinking} 170 + className="text-tertiary hover:text-accent-contrast shrink-0" 171 + onClick={async () => { 172 + setUnlinking(true); 173 + // Optimistically remove domain from publication data 174 + props.mutatePubData( 175 + (current) => { 176 + if (!current) return current; 177 + let pub = current.publication; 178 + if (!pub) return current; 179 + return { 180 + ...current, 181 + publication: { 182 + ...pub, 183 + publication_domains: ( 184 + pub.publication_domains || [] 185 + ).filter((d) => d.domain !== props.domain), 186 + }, 187 + }; 188 + }, 189 + { revalidate: false }, 190 + ); 191 + // Optimistically remove publication assignment from identity data 192 + mutateIdentityData(props.mutateIdentity, (draft) => { 193 + let domain = draft.custom_domains.find( 194 + (d) => d.domain === props.domain, 195 + ); 196 + if (domain) { 197 + domain.publication_domains = []; 198 + } 199 + }); 200 + toaster({ 201 + content: ( 202 + <div> 203 + Unlinked <strong>{props.domain}</strong> 204 + </div> 205 + ), 206 + type: "success", 207 + }); 208 + await removeDomainAssignment({ domain: props.domain }); 209 + props.mutatePubData(); 210 + props.mutateIdentity(); 211 + setUnlinking(false); 212 + }} 213 + > 214 + {unlinking ? ( 215 + <DotLoader className="h-[16px]! text-xs" /> 216 + ) : ( 217 + <UnlinkTiny /> 218 + )} 219 + </button> 220 + )} 221 + <button 222 + type="button" 223 + disabled={loading} 224 + onClick={async () => { 225 + setLoading(true); 226 + // Optimistically update the record's url so the UI 227 + // immediately reflects the new default domain 228 + props.mutatePubData( 229 + (current) => { 230 + if (!current) return current; 231 + let pub = current.publication; 232 + if (!pub?.record) return current; 233 + let rec = pub.record as Record<string, unknown>; 234 + if (typeof rec.url !== "string") return current; 235 + let protocol = 236 + rec.url.match(/^https?:\/\//)?.[0] || "https://"; 237 + return { 238 + ...current, 239 + publication: { 240 + ...pub, 241 + record: { ...rec, url: protocol + props.domain }, 242 + }, 243 + }; 244 + }, 245 + { revalidate: false }, 246 + ); 247 + toaster({ 248 + content: ( 249 + <div> 250 + Default domain set to <strong>{props.domain}</strong> 251 + </div> 252 + ), 253 + type: "success", 254 + }); 255 + await updatePublicationBasePath({ 256 + uri: props.publication_uri, 257 + base_path: props.domain, 258 + }); 259 + props.mutatePubData(); 260 + setLoading(false); 261 + }} 262 + className="hover:text-accent-contrast" 263 + > 264 + {loading ? ( 265 + <DotLoader className="h-[18px]! text-xs" /> 266 + ) : ( 267 + <PinTiny className="text-tertiary hover:text-accent-contrast group-hover/domain:text-accent-2 shrink-0" /> 268 + )} 269 + </button> 270 + </div> 261 271 )} 262 - </button> 272 + </> 263 273 )} 264 274 </div> 265 275 </div> ··· 269 279 function UnassignedDomainRow(props: { 270 280 domainData: CustomDomain; 271 281 publication_uri: string; 282 + mutatePubData: ReturnType<typeof usePublicationData>["mutate"]; 272 283 onAssigned: () => void; 273 284 onSettings: () => void; 274 285 }) { ··· 300 311 ]; 301 312 } 302 313 }); 314 + props.mutatePubData( 315 + (current) => { 316 + if (!current) return current; 317 + let pub = current.publication; 318 + if (!pub) return current; 319 + return { 320 + ...current, 321 + publication: { 322 + ...pub, 323 + publication_domains: [ 324 + ...(pub.publication_domains || []), 325 + { 326 + publication: props.publication_uri, 327 + domain: props.domainData.domain, 328 + created_at: new Date().toISOString(), 329 + identity: "", 330 + }, 331 + ], 332 + }, 333 + }; 334 + }, 335 + { revalidate: false }, 336 + ); 303 337 setConfirming(false); 304 338 props.onAssigned(); 305 339 await assignDomainToPublication({ ··· 319 353 {props.domainData.domain} 320 354 </button> 321 355 {pending ? ( 322 - <button 323 - className="text-accent-contrast text-xs" 324 - onClick={() => props.onSettings()} 325 - type="button" 326 - > 327 - pending 328 - </button> 356 + <div className="text-tertiary animate-pulse text-xs">unverfied</div> 329 357 ) : confirming ? null : ( 330 358 <button 331 359 className="text-accent-contrast text-xs font-bold"
+2 -2
components/Domains/domainAssignment.ts
··· 6 6 | { type: "document"; routes: string[] }; 7 7 8 8 export function getDomainAssignment(domain: CustomDomain): DomainAssignment { 9 - if (domain.publication_domains.length > 0) { 9 + if (domain.publication_domains && domain.publication_domains.length > 0) { 10 10 return { type: "publication" }; 11 11 } 12 - if (domain.custom_domain_routes.length > 0) { 12 + if (domain.custom_domain_routes && domain.custom_domain_routes.length > 0) { 13 13 return { 14 14 type: "document", 15 15 routes: domain.custom_domain_routes.map((r) => r.route),
+3 -1
components/Popover/index.tsx
··· 43 43 <RadixPopover.Content 44 44 className={` 45 45 z-20 relative bg-bg-page 46 - px-3 py-2 text-primary 46 + text-primary 47 + flex flex-col overflow-hidden 48 + px-3 py-2 47 49 max-w-(--radix-popover-content-available-width) 48 50 max-h-(--radix-popover-content-available-height) 49 51 border border-border rounded-md shadow-md
+4 -4
app/[leaflet_id]/actions/PublishButton.tsx
··· 185 185 onOpenChange={(o) => setOpen(o)} 186 186 side={isMobile ? "top" : "right"} 187 187 align={isMobile ? "center" : "start"} 188 - className="sm:max-w-sm w-[1000px]" 188 + className="sm:max-w-sm w-[1000px] p-0!" 189 189 trigger={ 190 190 <ActionButton 191 191 primary ··· 195 195 } 196 196 > 197 197 {!identity || !identity.atp_did ? ( 198 - <div className="-mx-2 -my-1"> 198 + <div className="p-1"> 199 199 <div 200 200 className={`bg-[var(--accent-light)] w-full rounded-md flex flex-col text-center justify-center p-2 pb-4 text-sm`} 201 201 > ··· 219 219 </div> 220 220 </div> 221 221 ) : ( 222 - <div className="flex flex-col"> 222 + <div className="publishToPubForm p-3 flex flex-col overflow-scroll max-h-full min-h-0"> 223 223 <PostDetailsForm 224 224 title={title} 225 225 description={description} ··· 310 310 setDescription: (d: string) => void; 311 311 }) => { 312 312 return ( 313 - <div className=" flex flex-col gap-1"> 313 + <div className="postDetailsContent flex flex-col gap-1 "> 314 314 <div className="text-sm text-tertiary">Post Details</div> 315 315 <div className="flex flex-col gap-2"> 316 316 <InputWithLabel label="Title" value={props.title} disabled />