a tool for shared writing and social publishing
0

Configure Feed

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

fix bugs in ui model

Jared Pereira (Mar 13, 2026, 4:17 PM EDT) 9920cf86 3c1c22aa

+228 -119
+7 -5
actions/domains/assignDomainToDocument.ts
··· 24 24 if (!identity || !identity.custom_domains.find((d) => d.domain === domain)) 25 25 return null; 26 26 27 - await supabase.from("publication_domains").delete().eq("domain", domain); 28 - await supabase 29 - .from("custom_domain_routes") 30 - .delete() 31 - .eq("edit_permission_token", edit_permission_token); 27 + await Promise.all([ 28 + supabase.from("publication_domains").delete().eq("domain", domain), 29 + supabase 30 + .from("custom_domain_routes") 31 + .delete() 32 + .eq("edit_permission_token", edit_permission_token), 33 + ]); 32 34 33 35 await supabase.from("custom_domain_routes").insert({ 34 36 domain,
+4 -2
actions/domains/assignDomainToPublication.ts
··· 27 27 .single(); 28 28 if (publication?.identity_did !== identity.atp_did) return null; 29 29 30 - await supabase.from("custom_domain_routes").delete().eq("domain", domain); 31 - await supabase.from("publication_domains").delete().eq("domain", domain); 30 + await Promise.all([ 31 + supabase.from("custom_domain_routes").delete().eq("domain", domain), 32 + supabase.from("publication_domains").delete().eq("domain", domain), 33 + ]); 32 34 33 35 await supabase.from("publication_domains").insert({ 34 36 publication: publication_uri,
+4 -2
actions/domains/removeDomainAssignment.ts
··· 18 18 if (!identity || !identity.custom_domains.find((d) => d.domain === domain)) 19 19 return null; 20 20 21 - await supabase.from("custom_domain_routes").delete().eq("domain", domain); 22 - await supabase.from("publication_domains").delete().eq("domain", domain); 21 + await Promise.all([ 22 + supabase.from("custom_domain_routes").delete().eq("domain", domain), 23 + supabase.from("publication_domains").delete().eq("domain", domain), 24 + ]); 23 25 24 26 return true; 25 27 }
+25
actions/domains/removeDomainRoute.ts
··· 1 + "use server"; 2 + import { Database } from "supabase/database.types"; 3 + import { createServerClient } from "@supabase/ssr"; 4 + import { getIdentityData } from "actions/getIdentityData"; 5 + 6 + let supabase = createServerClient<Database>( 7 + process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, 8 + process.env.SUPABASE_SERVICE_ROLE_KEY as string, 9 + { cookies: {} }, 10 + ); 11 + 12 + export async function removeDomainRoute({ routeId }: { routeId: string }) { 13 + let identity = await getIdentityData(); 14 + if (!identity) return null; 15 + 16 + // Verify the route belongs to one of the user's domains 17 + let allRoutes = identity.custom_domains.flatMap( 18 + (d) => d.custom_domain_routes, 19 + ); 20 + if (!allRoutes.find((r) => r.id === routeId)) return null; 21 + 22 + await supabase.from("custom_domain_routes").delete().eq("id", routeId); 23 + 24 + return true; 25 + }
+91 -19
components/Domains/DomainSettingsView.tsx
··· 1 1 "use client"; 2 2 import { useState } from "react"; 3 - import { ButtonPrimary } from "components/Buttons"; 4 3 import { useDomainStatus } from "./useDomainStatus"; 5 4 import { DotLoader } from "components/utils/DotLoader"; 6 5 import { deleteDomain } from "actions/domains/deleteDomain"; 7 6 import { removeDomainAssignment } from "actions/domains/removeDomainAssignment"; 8 - import { useIdentityData } from "components/IdentityProvider"; 7 + import { removeDomainRoute } from "actions/domains/removeDomainRoute"; 8 + import { 9 + useIdentityData, 10 + mutateIdentityData, 11 + } from "components/IdentityProvider"; 12 + import { getDomainAssignment } from "./domainAssignment"; 9 13 10 14 export function DomainSettingsView(props: { 11 15 domain: string; ··· 13 17 onRemoveAssignment?: () => void; 14 18 onDeleteDomain?: () => void; 15 19 }) { 16 - let { data, pending: needsSetup, mutate: mutateDomainStatus } = useDomainStatus(props.domain); 17 - let { mutate: mutateIdentity } = useIdentityData(); 20 + let { 21 + data, 22 + pending: needsSetup, 23 + mutate: mutateDomainStatus, 24 + } = useDomainStatus(props.domain); 25 + let { identity, mutate: mutateIdentity } = useIdentityData(); 18 26 let isSubdomain = props.domain.split(".").length > 2; 27 + 28 + let domainData = identity?.custom_domains.find( 29 + (d) => d.domain === props.domain, 30 + ); 31 + let assignment = domainData ? getDomainAssignment(domainData) : null; 32 + let isAssigned = assignment && assignment.type !== "unassigned"; 19 33 20 34 return ( 21 35 <div className="flex flex-col gap-[6px] text-sm text-primary max-w-full"> ··· 100 114 <div className="text-green-600">This domain is verified!</div> 101 115 )} 102 116 117 + {/* Assignment list */} 118 + {props.onRemoveAssignment && isAssigned && domainData && ( 119 + <div className="flex flex-col gap-1 mt-1"> 120 + <h4 className="text-tertiary text-xs">Assigned to</h4> 121 + {domainData.publication_domains.length > 0 && ( 122 + <div className="flex items-center justify-between px-[6px] py-1 border rounded-md border-border-light"> 123 + <span className="text-secondary">publication</span> 124 + <button 125 + className="text-accent-contrast text-xs" 126 + type="button" 127 + onMouseDown={async () => { 128 + mutateIdentityData(mutateIdentity, (draft) => { 129 + let domain = draft.custom_domains.find( 130 + (d) => d.domain === props.domain, 131 + ); 132 + if (domain) { 133 + domain.publication_domains = []; 134 + } 135 + }); 136 + props.onRemoveAssignment?.(); 137 + await removeDomainAssignment({ domain: props.domain }); 138 + }} 139 + > 140 + remove 141 + </button> 142 + </div> 143 + )} 144 + {domainData.custom_domain_routes.map((route) => ( 145 + <div 146 + key={route.id} 147 + className="flex items-center justify-between px-[6px] py-1 border rounded-md border-border-light" 148 + > 149 + <a 150 + href={`/${route.edit_permission_token}`} 151 + className="text-accent-contrast hover:underline truncate" 152 + > 153 + {route.route} 154 + </a> 155 + <button 156 + className="text-accent-contrast text-xs shrink-0 ml-2" 157 + type="button" 158 + onMouseDown={async () => { 159 + mutateIdentityData(mutateIdentity, (draft) => { 160 + let domain = draft.custom_domains.find( 161 + (d) => d.domain === props.domain, 162 + ); 163 + if (domain) { 164 + domain.custom_domain_routes = 165 + domain.custom_domain_routes.filter( 166 + (r) => r.id !== route.id, 167 + ); 168 + } 169 + }); 170 + // If this was the last route, go back to the list 171 + if (domainData.custom_domain_routes.length <= 1) { 172 + props.onRemoveAssignment?.(); 173 + } 174 + await removeDomainRoute({ routeId: route.id }); 175 + }} 176 + > 177 + remove 178 + </button> 179 + </div> 180 + ))} 181 + </div> 182 + )} 183 + 103 184 <div className="flex flex-col gap-2 mt-2"> 104 185 <div className="flex gap-3 justify-between items-center"> 105 186 <button ··· 119 200 <hr className="border-border-light" /> 120 201 121 202 <div className="flex gap-3 justify-between items-center"> 122 - {props.onRemoveAssignment && ( 123 - <button 124 - className="text-accent-contrast text-sm" 125 - type="button" 126 - onMouseDown={async () => { 127 - await removeDomainAssignment({ domain: props.domain }); 128 - mutateIdentity(); 129 - props.onRemoveAssignment?.(); 130 - }} 131 - > 132 - Remove Assignment 133 - </button> 134 - )} 135 203 {props.onDeleteDomain && ( 136 204 <button 137 205 className="text-accent-contrast font-bold text-sm" 138 206 type="button" 139 207 onMouseDown={async () => { 140 - await deleteDomain({ domain: props.domain }); 141 - mutateIdentity(); 208 + mutateIdentityData(mutateIdentity, (draft) => { 209 + draft.custom_domains = draft.custom_domains.filter( 210 + (d) => d.domain !== props.domain, 211 + ); 212 + }); 142 213 props.onDeleteDomain?.(); 214 + await deleteDomain({ domain: props.domain }); 143 215 }} 144 216 > 145 217 Delete Domain
+23 -3
components/Domains/PublicationDomains.tsx
··· 3 3 import { mutate } from "swr"; 4 4 import { useDomainStatus } from "./useDomainStatus"; 5 5 import { getDomainAssignment, describeAssignment } from "./domainAssignment"; 6 - import { useIdentityData } from "components/IdentityProvider"; 6 + import { 7 + useIdentityData, 8 + mutateIdentityData, 9 + } from "components/IdentityProvider"; 7 10 import { ButtonPrimary } from "components/Buttons"; 8 11 import { 9 12 usePublicationData, ··· 194 197 onSettings: () => void; 195 198 }) { 196 199 let { pending } = useDomainStatus(props.domainData.domain); 200 + let { mutate: mutateIdentity } = useIdentityData(); 197 201 let assignment = getDomainAssignment(props.domainData); 198 202 let [confirming, setConfirming] = useState(false); 199 203 200 204 async function doAssign() { 205 + mutateIdentityData(mutateIdentity, (draft) => { 206 + let domain = draft.custom_domains.find( 207 + (d) => d.domain === props.domainData.domain, 208 + ); 209 + if (domain) { 210 + domain.custom_domain_routes = []; 211 + domain.publication_domains = [ 212 + { 213 + publication: props.publication_uri, 214 + domain: props.domainData.domain, 215 + identity: "", 216 + created_at: new Date().toISOString(), 217 + }, 218 + ]; 219 + } 220 + }); 221 + setConfirming(false); 222 + props.onAssigned(); 201 223 await assignDomainToPublication({ 202 224 domain: props.domainData.domain, 203 225 publication_uri: props.publication_uri, 204 226 }); 205 - setConfirming(false); 206 - props.onAssigned(); 207 227 } 208 228 209 229 return (
+3 -3
components/Domains/domainAssignment.ts
··· 21 21 export function describeAssignment(assignment: DomainAssignment): string { 22 22 switch (assignment.type) { 23 23 case "publication": 24 - return "a publication"; 24 + return "publication"; 25 25 case "document": 26 26 return assignment.routes.length === 1 27 - ? `a leaflet at ${assignment.routes[0]}` 28 - : `${assignment.routes.length} leaflets`; 27 + ? "1 page" 28 + : `${assignment.routes.length} pages`; 29 29 case "unassigned": 30 30 return ""; 31 31 }
+71 -85
app/[leaflet_id]/actions/ShareOptions/DomainOptions.tsx
··· 2 2 import { ButtonPrimary } from "components/Buttons"; 3 3 import { useToaster } from "components/Toast"; 4 4 import { Input } from "components/Input"; 5 - import { useIdentityData } from "components/IdentityProvider"; 5 + import { 6 + useIdentityData, 7 + mutateIdentityData, 8 + } from "components/IdentityProvider"; 6 9 import { useDomainStatus } from "components/Domains/useDomainStatus"; 7 10 import { CustomDomain } from "components/Domains/DomainList"; 8 11 import { ··· 85 88 let toaster = useToaster(); 86 89 let publishLink = useReadOnlyShareLink(); 87 90 88 - // Find the CustomDomain object for the selected domain to check its assignment 89 - let selectedDomainData = identity?.custom_domains.find( 90 - (d: CustomDomain) => d.domain === selectedDomain, 91 + // Filter out domains assigned to publications — they can't be used for leaflets 92 + let availableDomains = (identity?.custom_domains || []).filter( 93 + (d: CustomDomain) => d.publication_domains.length === 0, 91 94 ); 92 - let selectedAssignment = selectedDomainData 93 - ? getDomainAssignment(selectedDomainData) 94 - : null; 95 - let willReassign = 96 - selectedAssignment && selectedAssignment.type !== "unassigned"; 97 - 98 - let [confirmingReassign, setConfirmingReassign] = useState(false); 99 95 100 96 async function doPublish() { 101 97 if (!selectedDomain || !publishLink) return; 102 - await assignDomainToDocument({ 103 - domain: selectedDomain, 104 - route: "/" + selectedRoute, 105 - view_permission_token: publishLink, 106 - edit_permission_token: permission_token.id, 98 + let route = "/" + selectedRoute; 99 + 100 + // Optimistic update 101 + mutateIdentityData(mutateIdentity, (draft) => { 102 + let domain = draft.custom_domains.find( 103 + (d) => d.domain === selectedDomain, 104 + ); 105 + if (domain) { 106 + // Remove existing route for this leaflet, add new one 107 + domain.custom_domain_routes = [ 108 + ...domain.custom_domain_routes.filter( 109 + (r) => r.edit_permission_token !== permission_token.id, 110 + ), 111 + { 112 + id: crypto.randomUUID(), 113 + domain: selectedDomain, 114 + route, 115 + view_permission_token: publishLink, 116 + edit_permission_token: permission_token.id, 117 + created_at: new Date().toISOString(), 118 + }, 119 + ]; 120 + } 107 121 }); 108 122 109 123 toaster({ ··· 121 135 ), 122 136 type: "success", 123 137 }); 124 - mutateIdentity(); 125 138 mutateDomains(); 126 - setConfirmingReassign(false); 127 139 props.setShareMenuState("default"); 140 + 141 + await assignDomainToDocument({ 142 + domain: selectedDomain, 143 + route, 144 + view_permission_token: publishLink, 145 + edit_permission_token: permission_token.id, 146 + }); 128 147 } 129 148 130 149 return ( 131 150 <div className="px-3 py-1 flex flex-col gap-3 max-w-full w-[600px]"> 132 151 <h3 className="text-secondary">Choose a Domain</h3> 133 152 <div className="flex flex-col gap-1 text-secondary"> 134 - {identity?.custom_domains.map((domain: CustomDomain) => { 153 + {availableDomains.map((domain: CustomDomain) => { 135 154 return ( 136 155 <DomainOption 137 156 selectedRoute={selectedRoute} ··· 139 158 key={domain.domain} 140 159 domain={domain} 141 160 checked={selectedDomain === domain.domain} 142 - setChecked={(d) => { 143 - setSelectedDomain(d); 144 - setConfirmingReassign(false); 145 - }} 161 + setChecked={(d) => setSelectedDomain(d)} 146 162 setDomainMenuState={props.setDomainMenuState} 147 163 /> 148 164 ); ··· 157 173 </button> 158 174 </div> 159 175 160 - {confirmingReassign && selectedAssignment && ( 161 - <div className="text-sm border border-accent-contrast rounded-md p-2 flex flex-col gap-2"> 162 - <p className="text-secondary"> 163 - <strong>{selectedDomain}</strong> is currently assigned to{" "} 164 - {describeAssignment(selectedAssignment)}. Publishing here will 165 - remove that assignment. 166 - </p> 167 - <div className="flex gap-2 justify-end"> 168 - <button 169 - className="text-accent-contrast text-sm" 170 - onMouseDown={() => setConfirmingReassign(false)} 171 - > 172 - Cancel 173 - </button> 174 - <ButtonPrimary compact onMouseDown={doPublish}> 175 - Reassign & Publish 176 - </ButtonPrimary> 177 - </div> 178 - </div> 179 - )} 180 - 181 - {!confirmingReassign && ( 182 - <div className="flex gap-3 items-center justify-end"> 183 - {props.domainConnected && ( 184 - <button 185 - onMouseDown={() => { 186 - props.setShareMenuState("default"); 187 - toaster({ 188 - content: ( 189 - <div className="font-bold"> 190 - Unpublished from custom domain! 191 - </div> 192 - ), 193 - type: "error", 194 - }); 195 - }} 196 - > 197 - Unpublish 198 - </button> 199 - )} 200 - 201 - <ButtonPrimary 202 - id="publish-to-domain" 203 - disabled={ 204 - domains?.[0] 205 - ? domains[0].domain === selectedDomain && 206 - domains[0].route.slice(1) === selectedRoute 207 - : !selectedDomain 208 - } 209 - onClick={async () => { 210 - if (willReassign) { 211 - setConfirmingReassign(true); 212 - return; 213 - } 214 - await doPublish(); 176 + <div className="flex gap-3 items-center justify-end"> 177 + {props.domainConnected && ( 178 + <button 179 + onMouseDown={() => { 180 + props.setShareMenuState("default"); 181 + toaster({ 182 + content: ( 183 + <div className="font-bold"> 184 + Unpublished from custom domain! 185 + </div> 186 + ), 187 + type: "error", 188 + }); 215 189 }} 216 190 > 217 - Publish! 218 - </ButtonPrimary> 219 - </div> 220 - )} 191 + Unpublish 192 + </button> 193 + )} 194 + 195 + <ButtonPrimary 196 + id="publish-to-domain" 197 + disabled={ 198 + domains?.[0] 199 + ? domains[0].domain === selectedDomain && 200 + domains[0].route.slice(1) === selectedRoute 201 + : !selectedDomain 202 + } 203 + onClick={doPublish} 204 + > 205 + Publish! 206 + </ButtonPrimary> 207 + </div> 221 208 </div> 222 209 ); 223 210 }; ··· 233 220 let [value, setValue] = useState(""); 234 221 let { pending } = useDomainStatus(props.domain.domain); 235 222 let assignment = getDomainAssignment(props.domain); 236 - let assignedElsewhere = assignment.type !== "unassigned"; 237 223 238 224 return ( 239 225 <label htmlFor={props.domain.domain}> ··· 258 244 pending 259 245 ? "border-border-light text-secondary justify-between gap-2 items-center " 260 246 : !props.checked 261 - ? `flex-wrap border-border-light ${assignedElsewhere ? "opacity-70" : ""}` 247 + ? "flex-wrap border-border-light" 262 248 : "flex-wrap border-accent-1 bg-accent-1 text-accent-2 font-bold" 263 249 } `} 264 250 > ··· 266 252 <div className={`w-max truncate ${pending && "animate-pulse"}`}> 267 253 {props.domain.domain} 268 254 </div> 269 - {!props.checked && assignedElsewhere && !pending && ( 255 + {!props.checked && assignment.type === "document" && !pending && ( 270 256 <span className="text-xs text-tertiary"> 271 257 {describeAssignment(assignment)} 272 258 </span>