a tool for shared writing and social publishing

WIP

+347 -344
-30
actions/createPublication.ts
··· 1 - "use server"; 2 - import { TID } from "@atproto/common"; 3 - import { AtpBaseClient } from "lexicons/api"; 4 - import { createOauthClient } from "src/atproto-oauth"; 5 - import { getIdentityData } from "actions/getIdentityData"; 6 - import { supabaseServerClient } from "supabase/serverClient"; 7 - 8 - export async function createPublication(name: string) { 9 - const oauthClient = await createOauthClient(); 10 - let identity = await getIdentityData(); 11 - if (!identity || !identity.atp_did) return; 12 - let credentialSession = await oauthClient.restore(identity.atp_did); 13 - let agent = new AtpBaseClient( 14 - credentialSession.fetchHandler.bind(credentialSession), 15 - ); 16 - let record = { 17 - name, 18 - }; 19 - let result = await agent.pub.leaflet.publication.create( 20 - { repo: credentialSession.did!, rkey: TID.nextStr(), validate: false }, 21 - record, 22 - ); 23 - 24 - //optimistically write to our db! 25 - await supabaseServerClient.from("publications").upsert({ 26 - uri: result.uri, 27 - identity_did: credentialSession.did!, 28 - name: record.name, 29 - }); 30 - }
+8
app/globals.css
··· 244 244 @apply outline-transparent; 245 245 } 246 246 247 + .container { 248 + @apply bg-bg-page; 249 + @apply border; 250 + @apply border-bg-page; 251 + @apply bg-opacity-50; 252 + @apply rounded-md; 253 + } 254 + 247 255 .pwa-padding { 248 256 padding-top: max(calc(env(safe-area-inset-top) - 8px)) !important; 249 257 }
+69
app/home/Publications.tsx
··· 1 + "use client"; 2 + import { ButtonPrimary } from "components/Buttons"; 3 + import Link from "next/link"; 4 + import { useState } from "react"; 5 + 6 + import { Input } from "components/Input"; 7 + import { useIdentityData } from "components/IdentityProvider"; 8 + import { NewDraftButton } from "app/lish/[handle]/[publication]/NewDraftButton"; 9 + import { Popover } from "components/Popover"; 10 + import { BlueskyLogin } from "app/login/LoginForm"; 11 + export const MyPublicationList = () => { 12 + let { identity } = useIdentityData(); 13 + if (!identity) return null; 14 + if (!identity.atp_did) 15 + return ( 16 + <div> 17 + <Popover 18 + asChild 19 + trigger={ 20 + <ButtonPrimary className="place-self-start text-sm"> 21 + Log in with bluesky to create a publication! 22 + </ButtonPrimary> 23 + } 24 + > 25 + <BlueskyLogin /> 26 + </Popover> 27 + </div> 28 + ); 29 + return ( 30 + <div className="w-full flex flex-col gap-2"> 31 + <PublicationList publications={identity.publications} /> 32 + <Link 33 + href={"./lish/createPub"} 34 + className="text-sm place-self-start text-tertiary hover:text-accent-contrast" 35 + > 36 + New Publication 37 + </Link> 38 + </div> 39 + ); 40 + }; 41 + 42 + const PublicationList = (props: { 43 + publications: { 44 + identity_did: string; 45 + indexed_at: string; 46 + name: string; 47 + uri: string; 48 + }[]; 49 + }) => { 50 + let { identity } = useIdentityData(); 51 + 52 + return ( 53 + <div className="w-full flex flex-col gap-2"> 54 + {props.publications?.map((d) => ( 55 + <div 56 + key={d.uri} 57 + className={`pubPostListItem flex hover:no-underline justify-between items-center`} 58 + > 59 + <Link 60 + className="justify-self-start font-bold hover:no-underline" 61 + href={`/lish/${identity?.resolved_did?.alsoKnownAs?.[0].slice(5)}/${d.name}/`} 62 + > 63 + <div key={d.uri}>{d.name}</div> 64 + </Link> 65 + </div> 66 + ))} 67 + </div> 68 + ); 69 + };
-9
app/home/page.tsx
··· 8 8 ThemeProvider, 9 9 } from "components/ThemeManager/ThemeProvider"; 10 10 import { EntitySetProvider } from "components/EntitySetProvider"; 11 - import { ThemePopover } from "components/ThemeManager/ThemeSetter"; 12 11 import { createIdentity } from "actions/createIdentity"; 13 12 import postgres from "postgres"; 14 13 import { drizzle } from "drizzle-orm/postgres-js"; 15 14 import { IdentitySetter } from "./IdentitySetter"; 16 - import { HomeHelp } from "./HomeHelp"; 17 15 import { LeafletList } from "./LeafletList"; 18 - import { CreateNewLeafletButton } from "./CreateNewButton"; 19 16 import { getIdentityData } from "actions/getIdentityData"; 20 - import { LoginButton } from "components/LoginButton"; 21 - import { HelpPopover } from "components/HelpPopover"; 22 - import { AccountSettings } from "./AccountSettings"; 23 - import { LoggedOutWarning } from "./LoggedOutWarning"; 24 17 import { getFactsFromHomeLeaflets } from "app/api/rpc/[command]/getFactsFromHomeLeaflets"; 25 - import { Media } from "components/Media"; 26 - import { Sidebar } from "components/ActionBar/Sidebar"; 27 18 import { HomeSidebar } from "./HomeSidebar"; 28 19 import { HomeFooter } from "./HomeFooter"; 29 20
-10
app/lish/AlphaBanner.tsx
··· 1 - import Link from "next/link"; 2 - 3 - export const AlphaBanner = () => { 4 - return ( 5 - <div className="w-full h-fit text-center bg-accent-1 text-accent-2"> 6 - We're still in Early Alpha! <Link href="./lish/">Sign Up</Link> for 7 - Updates :) 8 - </div> 9 - ); 10 - };
-214
app/lish/LishHome.tsx
··· 1 - "use client"; 2 - import { ButtonPrimary } from "components/Buttons"; 3 - import Link from "next/link"; 4 - import { useState } from "react"; 5 - import { PostList } from "./PostList"; 6 - 7 - import { Input } from "components/Input"; 8 - import { useIdentityData } from "components/IdentityProvider"; 9 - import { NewDraftButton } from "./[handle]/[publication]/NewDraftButton"; 10 - 11 - export const LishHome = () => { 12 - let [state, setState] = useState<"posts" | "subscriptions">("posts"); 13 - return ( 14 - <div className="w-full h-fit min-h-full p-4 bg-bg-leaflet"> 15 - <div className="flex flex-col gap-6 justify-center place-items-center max-w-prose w-full mx-auto"> 16 - <div 17 - className="p-4 rounded-md w-full" 18 - style={{ 19 - backgroundColor: 20 - "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 85%)", 21 - }} 22 - > 23 - <MyPublicationList /> 24 - </div> 25 - 26 - {/* <div className="homeFeed w-full flex flex-col"> 27 - <div className="flex gap-1 justify-center pb-2"> 28 - <Tab 29 - name="updates" 30 - active={state === "posts"} 31 - onClick={() => setState("posts")} 32 - /> 33 - <Tab 34 - name="subscriptions" 35 - active={state === "subscriptions"} 36 - onClick={() => setState("subscriptions")} 37 - /> 38 - </div> 39 - <hr className="border-border w-full mb-2" /> 40 - {state === "posts" ? ( 41 - <PostFeed /> 42 - ) : ( 43 - <SubscriptionList publications={Subscriptions} /> 44 - )} 45 - </div> */} 46 - </div> 47 - </div> 48 - ); 49 - }; 50 - 51 - const Tab = (props: { name: string; active: boolean; onClick: () => void }) => { 52 - return ( 53 - <div 54 - className={`border-border px-2 py-1 ${props.active ? "font-bold" : ""}`} 55 - onClick={props.onClick} 56 - > 57 - {props.name} 58 - </div> 59 - ); 60 - }; 61 - 62 - const MyPublicationList = () => { 63 - let { identity } = useIdentityData(); 64 - if (!identity || !identity?.atp_did) { 65 - return ( 66 - <div className="flex flex-col justify-center text-center place-items-center"> 67 - <div className="font-bold text-center"> 68 - Connect to Bluesky <br className="sm:hidden" /> 69 - to start publishing! 70 - </div> 71 - <small className="text-secondary text-center pt-1"> 72 - We use the ATProtocol to store all your publication data on the open 73 - web. That means we cannot lock you into our platform, you will ALWAYS 74 - be free to easily move elsewhere. <a>Learn More.</a> 75 - </small> 76 - <form 77 - action="/api/oauth/login?redirect_url=/lish" 78 - method="GET" 79 - className="relative w-fit mt-4 " 80 - > 81 - <Input 82 - type="text" 83 - className="input-with-border !pr-[88px] !py-1 grow w-full" 84 - name="handle" 85 - placeholder="Enter Bluesky handle..." 86 - required 87 - /> 88 - <ButtonPrimary 89 - compact 90 - className="absolute right-1 top-1 !outline-0" 91 - type="submit" 92 - > 93 - Connect 94 - </ButtonPrimary> 95 - </form> 96 - </div> 97 - ); 98 - } 99 - if (Publications.length === 0) { 100 - return ( 101 - <div> 102 - <Link href={"./lish/createPub"}> 103 - <ButtonPrimary>Start a Publication!</ButtonPrimary> 104 - </Link> 105 - </div> 106 - ); 107 - } 108 - return ( 109 - <div className="w-full flex flex-col gap-2"> 110 - <PublicationList publications={identity.publications} /> 111 - <Link 112 - href={"./lish/createPub"} 113 - className="text-sm place-self-start text-tertiary hover:text-accent-contrast" 114 - > 115 - New Publication 116 - </Link> 117 - </div> 118 - ); 119 - }; 120 - 121 - const PublicationList = (props: { 122 - publications: { 123 - identity_did: string; 124 - indexed_at: string; 125 - name: string; 126 - uri: string; 127 - }[]; 128 - }) => { 129 - let { identity } = useIdentityData(); 130 - 131 - return ( 132 - <div className="w-full flex flex-col gap-2"> 133 - {props.publications?.map((d) => ( 134 - <div 135 - key={d.uri} 136 - className={`pubPostListItem flex hover:no-underline justify-between items-center`} 137 - > 138 - <Link 139 - className="justify-self-start font-bold hover:no-underline" 140 - href={`/lish/${identity?.resolved_did?.alsoKnownAs?.[0].slice(5)}/${d.name}/`} 141 - > 142 - <div key={d.uri}>{d.name}</div> 143 - </Link> 144 - <NewDraftButton publication={d.uri} /> 145 - </div> 146 - ))} 147 - </div> 148 - ); 149 - }; 150 - 151 - const SubscriptionList = (props: { 152 - publications: { title: string; description: string }[]; 153 - }) => { 154 - if (props.publications.length === 0) 155 - return ( 156 - <div className="w-full text-center text-tertiary italic pt-4"> 157 - No subscriptions yet! 158 - </div> 159 - ); 160 - return ( 161 - <div className="w-full flex flex-col gap-2"> 162 - {props.publications.map((pub) => { 163 - return <SubscriptionListItem {...pub} />; 164 - })} 165 - </div> 166 - ); 167 - }; 168 - 169 - const SubscriptionListItem = (props: { 170 - title: string; 171 - description: string; 172 - }) => { 173 - return ( 174 - <Link 175 - href="./lish/publication" 176 - className={`pubPostListItem flex flex-col hover:no-underline justify-between items-center`} 177 - > 178 - <h4 className="justify-self-start">{props.title}</h4> 179 - 180 - <div className="text-secondary text-sm pt-1">{props.description}</div> 181 - <hr className="border-border-light mt-3" /> 182 - </Link> 183 - ); 184 - }; 185 - 186 - const PostFeed = () => { 187 - return <PostList isFeed posts={[]} />; 188 - }; 189 - 190 - let Subscriptions = [ 191 - { 192 - title: "vrk loves paper", 193 - description: 194 - "Exploring software that loves paper as much as I do. I'll be documenting my learnings, loves, confusions and creations in this newsletter!", 195 - }, 196 - { 197 - title: "rhrizome r&d", 198 - description: 199 - "Design, research, and complexity. A field guide for novel problems.", 200 - }, 201 - { 202 - title: "Dead Languages Society ", 203 - description: 204 - "A guided tour through the history of English and its relatives.", 205 - }, 206 - ]; 207 - 208 - let Publications = [ 209 - { 210 - title: "Leaflet Explorers", 211 - description: 212 - "We're making Leaflet, a fast fun web app for making delightful documents. Sign up to follow along as we build Leaflet! We send updates every week or two", 213 - }, 214 - ];
+44
app/lish/[handle]/[publication]/DraftList.tsx
··· 1 + "use client"; 2 + 3 + import { Fact, ReplicacheProvider } from "src/replicache"; 4 + import { usePublicationRelationship } from "./usePublicationRelationship"; 5 + import { usePublicationContext } from "components/Providers/PublicationContext"; 6 + import Link from "next/link"; 7 + 8 + export function DraftList(props: { 9 + drafts: { id: string; initialFacts: Fact<any>[]; root_entity: string }[]; 10 + }) { 11 + let rel = usePublicationRelationship(); 12 + let { publication } = usePublicationContext(); 13 + if (!publication) return null; 14 + if (!rel?.isAuthor) return null; 15 + return ( 16 + <div className=""> 17 + <h2>drafts</h2> 18 + {props.drafts.map((d) => { 19 + return ( 20 + <ReplicacheProvider 21 + key={d.id} 22 + rootEntity={d.root_entity} 23 + initialFacts={d.initialFacts} 24 + token={{ 25 + ...d, 26 + permission_token_rights: [], 27 + }} 28 + name={d.id} 29 + > 30 + <Draft id={d.id} /> 31 + </ReplicacheProvider> 32 + ); 33 + })} 34 + </div> 35 + ); 36 + } 37 + 38 + function Draft(props: { id: string }) { 39 + return ( 40 + <Link key={props.id} href={`/${props.id}`}> 41 + {props.id} 42 + </Link> 43 + ); 44 + }
app/lish/[handle]/[publication]/PostsList.tsx

This is a binary file and will not be displayed.

+26 -21
app/lish/[handle]/[publication]/page.tsx
··· 2 2 import { PostList } from "../../PostList"; 3 3 import { getPds, IdResolver } from "@atproto/identity"; 4 4 import { supabaseServerClient } from "supabase/serverClient"; 5 - import { AtUri } from "@atproto/syntax"; 6 - import { 7 - AtpBaseClient, 8 - PubLeafletDocument, 9 - PubLeafletPublication, 10 - } from "lexicons/api"; 11 5 import { CallToActionButton } from "./CallToActionButton"; 12 6 import { Metadata } from "next"; 7 + import { Fact } from "src/replicache"; 8 + import { Attributes } from "src/replicache/attributes"; 9 + import { DraftList } from "./DraftList"; 13 10 14 11 const idResolver = new IdResolver(); 15 12 ··· 36 33 }) { 37 34 let did = await idResolver.handle.resolve((await props.params).handle); 38 35 if (!did) return <PubNotFound />; 39 - 40 36 let { data: publication } = await supabaseServerClient 41 37 .from("publications") 42 38 .select( ··· 47 43 .single(); 48 44 if (!publication) return <PubNotFound />; 49 45 50 - let repo = await idResolver.did.resolve(did); 51 - if (!repo) return <PubNotFound />; 52 - const pds = getPds(repo); 53 - let agent = new AtpBaseClient((url, init) => { 54 - return fetch(new URL(url, pds), init); 46 + let all_facts = await supabaseServerClient.rpc("get_facts_for_roots", { 47 + max_depth: 2, 48 + roots: publication.leaflets_in_publications.map( 49 + (l) => l.permission_tokens?.root_entity!, 50 + ), 55 51 }); 52 + let facts = 53 + all_facts.data?.reduce( 54 + (acc, fact) => { 55 + if (!acc[fact.root_id]) acc[fact.root_id] = []; 56 + acc[fact.root_id].push( 57 + fact as unknown as Fact<keyof typeof Attributes>, 58 + ); 59 + return acc; 60 + }, 61 + {} as { [key: string]: Fact<keyof typeof Attributes>[] }, 62 + ) || {}; 56 63 57 64 try { 58 - let uri = new AtUri(publication.uri); 59 - let publication_record = await agent.pub.leaflet.publication.get({ 60 - repo: (await props.params).handle, 61 - rkey: uri.rkey, 62 - }); 63 - if (!PubLeafletPublication.isRecord(publication_record.value)) { 64 - return <pre>not a publication?</pre>; 65 - } 66 - 67 65 return ( 68 66 <div className="pubPage w-full h-screen bg-bg-leaflet flex items-stretch"> 69 67 <div className="pubWrapper flex flex-col w-full "> ··· 77 75 <CallToActionButton /> 78 76 </div> 79 77 </div> 78 + <DraftList 79 + drafts={publication.leaflets_in_publications.map((d) => ({ 80 + ...d.permission_tokens!, 81 + initialFacts: facts[d.permission_tokens?.root_entity!], 82 + }))} 83 + /> 84 + 80 85 <PostList posts={publication.documents_in_publications} /> 81 86 </div> 82 87 <Footer pageType="pub" />
+56 -18
app/lish/createPub/CreatePubForm.tsx
··· 1 1 "use client"; 2 - import { createPublication } from "actions/createPublication"; 2 + import { createPublication } from "./createPublication"; 3 3 import { ButtonPrimary } from "components/Buttons"; 4 + import { AddSmall } from "components/Icons"; 4 5 import { useIdentityData } from "components/IdentityProvider"; 5 6 import { InputWithLabel } from "components/Input"; 6 7 import Link from "next/link"; 7 8 import { useParams, useRouter } from "next/navigation"; 8 - import { useState } from "react"; 9 + import { useState, useRef } from "react"; 9 10 10 11 export const CreatePubForm = () => { 11 12 let [nameValue, setNameValue] = useState(""); 12 13 let [descriptionValue, setDescriptionValue] = useState(""); 14 + let [logoFile, setLogoFile] = useState<File | null>(null); 15 + let [logoPreview, setLogoPreview] = useState<string | null>(null); 16 + let fileInputRef = useRef<HTMLInputElement>(null); 13 17 14 18 let router = useRouter(); 15 19 let { identity } = useIdentityData(); 16 20 return ( 17 21 <form 18 - className="createPubForm w-full flex flex-col gap-3 bg-bg-page rounded-lg p-3 border border-border-light" 22 + className="container w-full flex flex-col gap-3 p-3 " 19 23 onSubmit={async (e) => { 20 24 e.preventDefault(); 21 - await createPublication(nameValue); 25 + // Note: You'll need to update the createPublication function to handle the logo file 26 + await createPublication({ 27 + name: nameValue, 28 + description: descriptionValue, 29 + logoFile, 30 + }); 22 31 router.push( 23 32 `/lish/${identity?.resolved_did?.alsoKnownAs?.[0].slice(5)}/${nameValue}/`, 24 33 ); 25 34 }} 26 35 > 36 + <div className="flex flex-col items-center mb-4 gap-2"> 37 + <div className="text-center text-secondary flex flex-col "> 38 + <h3 className="-mb-1">Logo</h3> 39 + <h3>(optional)</h3> 40 + </div> 41 + <div 42 + 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" 43 + onClick={() => fileInputRef.current?.click()} 44 + > 45 + {logoPreview ? ( 46 + <img 47 + src={logoPreview} 48 + alt="Logo preview" 49 + className="w-full h-full rounded-full object-cover" 50 + /> 51 + ) : ( 52 + <AddSmall className="text-accent-1" /> 53 + )} 54 + </div> 55 + <input 56 + type="file" 57 + accept="image/*" 58 + className="hidden" 59 + ref={fileInputRef} 60 + onChange={(e) => { 61 + const file = e.target.files?.[0]; 62 + if (file) { 63 + setLogoFile(file); 64 + const reader = new FileReader(); 65 + reader.onload = (e) => { 66 + setLogoPreview(e.target?.result as string); 67 + }; 68 + reader.readAsDataURL(file); 69 + } 70 + }} 71 + /> 72 + </div> 27 73 <InputWithLabel 28 74 type="text" 29 75 id="pubName" 30 - label="Name" 76 + label="Publication Name" 31 77 value={nameValue} 32 78 onChange={(e) => { 33 79 setNameValue(e.currentTarget.value); 34 80 }} 35 81 /> 36 82 37 - {/* <InputWithLabel 38 - label="Description" 83 + <InputWithLabel 84 + label="Description (optional)" 39 85 textarea 40 86 rows={3} 41 87 id="pubDescription" ··· 43 89 onChange={(e) => { 44 90 setDescriptionValue(e.currentTarget.value); 45 91 }} 46 - /> */} 92 + /> 47 93 48 - <div className="flex justify-between items-center"> 49 - <Link 50 - className="hover:no-underline font-bold text-accent-contrast" 51 - href="./" 52 - > 53 - Nevermind 54 - </Link> 55 - <ButtonPrimary className="place-self-end" type="submit"> 56 - Create! 57 - </ButtonPrimary> 94 + <div className="flex w-full justify-center"> 95 + <ButtonPrimary type="submit">Create Publication!</ButtonPrimary> 58 96 </div> 59 97 </form> 60 98 );
+61
app/lish/createPub/createPublication.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 { Un$Typed } from "@atproto/api"; 8 + 9 + export async function createPublication({ 10 + name, 11 + description, 12 + logoFile, 13 + }: { 14 + name: string; 15 + description: string; 16 + logoFile: File | null; 17 + }) { 18 + console.log(logoFile); 19 + return; 20 + const oauthClient = await createOauthClient(); 21 + let identity = await getIdentityData(); 22 + if (!identity || !identity.atp_did) return; 23 + let credentialSession = await oauthClient.restore(identity.atp_did); 24 + let agent = new AtpBaseClient( 25 + credentialSession.fetchHandler.bind(credentialSession), 26 + ); 27 + let record: Un$Typed<PubLeafletPublication.Record> = { 28 + name, 29 + }; 30 + 31 + if (description) { 32 + record.description = description; 33 + } 34 + 35 + // Upload the icon if provided 36 + if (iconFile && iconFile.size > 0) { 37 + const buffer = await iconFile.arrayBuffer(); 38 + const uploadResult = await agent.com.atproto.repo.uploadBlob( 39 + new Uint8Array(buffer), 40 + { encoding: iconFile.type }, 41 + ); 42 + 43 + if (uploadResult.data.blob) { 44 + record.icon = uploadResult.data.blob; 45 + } 46 + } 47 + 48 + let result = await agent.pub.leaflet.publication.create( 49 + { repo: credentialSession.did!, rkey: TID.nextStr(), validate: false }, 50 + record, 51 + ); 52 + 53 + //optimistically write to our db! 54 + await supabaseServerClient.from("publications").upsert({ 55 + uri: result.uri, 56 + identity_did: credentialSession.did!, 57 + name: record.name, 58 + }); 59 + 60 + return { success: true, name }; 61 + }
+2 -5
app/lish/createPub/page.tsx
··· 1 1 import { CreatePubForm } from "./CreatePubForm"; 2 - import { createClient } from "@supabase/supabase-js"; 3 - import { Database } from "supabase/database.types"; 4 - import { IdResolver } from "@atproto/identity"; 5 2 6 - export default function CreatePub() { 3 + export default async function CreatePub() { 7 4 return ( 8 5 <div className="createPubPage relative w-full h-screen flex items-stretch bg-bg-leaflet p-4"> 9 6 <div className="createPubContent h-full flex items-center max-w-sm w-full mx-auto "> 10 7 <div className="createPubFormWrapper h-fit w-full flex flex-col gap-4"> 11 - <h2 className="text-center">Create a New Publication</h2> 8 + <h2 className="text-center">Create Your Publication!</h2> 12 9 <CreatePubForm /> 13 10 </div> 14 11 </div>
-13
app/lish/page.tsx
··· 1 - import { LishHome } from "./LishHome"; 2 - import { createClient } from "@supabase/supabase-js"; 3 - import { Database } from "supabase/database.types"; 4 - import { AtpBaseClient, PubLeafletPagesLinearDocument } from "lexicons/api"; 5 - import { CredentialSession } from "@atproto/api"; 6 - import { createOauthClient } from "src/atproto-oauth"; 7 - import { getIdentityData } from "actions/getIdentityData"; 8 - import Link from "next/link"; 9 - import { IdResolver } from "@atproto/identity"; 10 - 11 - export default function Lish() { 12 - return <LishHome />; 13 - }
+1 -1
app/login/LoginForm.tsx
··· 157 157 ); 158 158 } 159 159 160 - function BlueskyLogin() { 160 + export function BlueskyLogin() { 161 161 const [signingWithHandle, setSigningWithHandle] = useState(false); 162 162 const [handle, setHandle] = useState(""); 163 163
+1 -1
appview/index.ts
··· 12 12 import { AtUri } from "@atproto/syntax"; 13 13 import { writeFile, readFile } from "fs/promises"; 14 14 15 - const cursorFile = "/cursor/cursor"; 15 + const cursorFile = process.env.CURSOR_FILE || "/cursor/cursor"; 16 16 17 17 let supabase = createClient<Database>( 18 18 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string,
+14 -14
components/Blocks/TextBlock/keymap.ts
··· 10 10 TextSelection, 11 11 Transaction, 12 12 } from "prosemirror-state"; 13 - import { MutableRefObject } from "react"; 13 + import { RefObject } from "react"; 14 14 import { Replicache } from "replicache"; 15 15 import { ReplicacheMutators } from "src/replicache"; 16 16 import { elementId } from "src/utils/elementId"; ··· 25 25 import { isTextBlock } from "src/utils/isTextBlock"; 26 26 import { UndoManager } from "src/undoManager"; 27 27 28 - type PropsRef = MutableRefObject<BlockProps & { entity_set: { set: string } }>; 28 + type PropsRef = RefObject<BlockProps & { entity_set: { set: string } }>; 29 29 export const TextBlockKeymap = ( 30 30 propsRef: PropsRef, 31 - repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 31 + repRef: RefObject<Replicache<ReplicacheMutators> | null>, 32 32 um: UndoManager, 33 33 ) => 34 34 ({ ··· 148 148 const moveCursorDown = 149 149 ( 150 150 propsRef: PropsRef, 151 - repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 151 + repRef: RefObject<Replicache<ReplicacheMutators> | null>, 152 152 jumpToNextBlock: boolean = false, 153 153 ) => 154 154 ( ··· 177 177 const moveCursorUp = 178 178 ( 179 179 propsRef: PropsRef, 180 - repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 180 + repRef: RefObject<Replicache<ReplicacheMutators> | null>, 181 181 jumpToNextBlock: boolean = false, 182 182 ) => 183 183 ( ··· 206 206 const backspace = 207 207 ( 208 208 propsRef: PropsRef, 209 - repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 209 + repRef: RefObject<Replicache<ReplicacheMutators> | null>, 210 210 ) => 211 211 ( 212 212 state: EditorState, ··· 352 352 353 353 const shifttab = 354 354 ( 355 - propsRef: MutableRefObject<BlockProps & { entity_set: { set: string } }>, 356 - repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 355 + propsRef: RefObject<BlockProps & { entity_set: { set: string } }>, 356 + repRef: RefObject<Replicache<ReplicacheMutators> | null>, 357 357 ) => 358 358 () => { 359 359 if (useUIState.getState().selectedBlocks.length > 1) return false; ··· 365 365 366 366 const enter = 367 367 ( 368 - propsRef: MutableRefObject<BlockProps & { entity_set: { set: string } }>, 369 - repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 368 + propsRef: RefObject<BlockProps & { entity_set: { set: string } }>, 369 + repRef: RefObject<Replicache<ReplicacheMutators> | null>, 370 370 ) => 371 371 ( 372 372 state: EditorState, ··· 579 579 580 580 const CtrlEnter = 581 581 ( 582 - propsRef: MutableRefObject<BlockProps & { entity_set: { set: string } }>, 583 - repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 582 + propsRef: RefObject<BlockProps & { entity_set: { set: string } }>, 583 + repRef: RefObject<Replicache<ReplicacheMutators> | null>, 584 584 ) => 585 585 ( 586 586 state: EditorState, ··· 595 595 596 596 const metaA = 597 597 ( 598 - propsRef: MutableRefObject<BlockProps & { entity_set: { set: string } }>, 599 - repRef: MutableRefObject<Replicache<ReplicacheMutators> | null>, 598 + propsRef: RefObject<BlockProps & { entity_set: { set: string } }>, 599 + repRef: RefObject<Replicache<ReplicacheMutators> | null>, 600 600 ) => 601 601 ( 602 602 state: EditorState,
+1 -1
components/Input.tsx
··· 69 69 JSX.IntrinsicElements["textarea"], 70 70 ) => { 71 71 let { label, ...inputProps } = props; 72 - let style = `appearance-none w-full font-normal bg-transparent text-base text-primary focus:outline-0 ${props.className}`; 72 + let style = `appearance-none w-full font-normal bg-transparent text-base text-primary focus:outline-0 ${props.className} outline-none resize-none`; 73 73 return ( 74 74 <label className=" input-with-border flex flex-col text-sm text-tertiary font-bold italic leading-tight !py-1 !px-[6px]"> 75 75 {props.label}
+1
lexicons/api/types/pub/leaflet/publication.ts
··· 14 14 $type: 'pub.leaflet.publication' 15 15 name: string 16 16 description?: string 17 + icon?: BlobRef 17 18 [k: string]: unknown 18 19 } 19 20
+7
lexicons/pub/leaflet/publication.json
··· 19 19 "description": { 20 20 "type": "string", 21 21 "maxLength": 2000 22 + }, 23 + "icon": { 24 + "type": "blob", 25 + "accept": [ 26 + "image/*" 27 + ], 28 + "maxSize": 1000000 22 29 } 23 30 } 24 31 }
+1
lexicons/src/publication.ts
··· 14 14 properties: { 15 15 name: { type: "string", maxLength: 2000 }, 16 16 description: { type: "string", maxLength: 2000 }, 17 + icon: { type: "blob", accept: ["image/*"], maxSize: 1000000 }, 17 18 }, 18 19 }, 19 20 },
+1
next.config.js
··· 30 30 ], 31 31 }, 32 32 experimental: { 33 + reactCompiler: true, 33 34 serverActions: { 34 35 bodySizeLimit: "5mb", 35 36 },
+51 -7
package-lock.json
··· 33 33 "@vercel/analytics": "^1.3.1", 34 34 "@vercel/kv": "^1.0.1", 35 35 "@vercel/sdk": "^1.3.1", 36 + "babel-plugin-react-compiler": "^19.1.0-rc.1", 36 37 "base64-js": "^1.5.1", 37 38 "colorjs.io": "^0.5.2", 38 39 "drizzle-orm": "^0.30.10", ··· 74 75 "@atproto/lex-cli": "^0.6.1", 75 76 "@atproto/lexicon": "^0.4.7", 76 77 "@cloudflare/workers-types": "^4.20240512.0", 78 + "@types/node": "^22.15.17", 77 79 "@types/react": "19.1.3", 78 80 "@types/react-dom": "19.1.3", 79 81 "@types/uuid": "^10.0.0", ··· 545 547 }, 546 548 "engines": { 547 549 "node": ">=18.7.0" 550 + } 551 + }, 552 + "node_modules/@babel/helper-string-parser": { 553 + "version": "7.27.1", 554 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", 555 + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", 556 + "license": "MIT", 557 + "engines": { 558 + "node": ">=6.9.0" 559 + } 560 + }, 561 + "node_modules/@babel/helper-validator-identifier": { 562 + "version": "7.27.1", 563 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", 564 + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", 565 + "license": "MIT", 566 + "engines": { 567 + "node": ">=6.9.0" 568 + } 569 + }, 570 + "node_modules/@babel/types": { 571 + "version": "7.27.1", 572 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", 573 + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", 574 + "license": "MIT", 575 + "dependencies": { 576 + "@babel/helper-string-parser": "^7.27.1", 577 + "@babel/helper-validator-identifier": "^7.27.1" 578 + }, 579 + "engines": { 580 + "node": ">=6.9.0" 548 581 } 549 582 }, 550 583 "node_modules/@badrap/valita": { ··· 5641 5674 "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" 5642 5675 }, 5643 5676 "node_modules/@types/node": { 5644 - "version": "20.12.12", 5645 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", 5646 - "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", 5677 + "version": "22.15.17", 5678 + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", 5679 + "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", 5680 + "license": "MIT", 5647 5681 "dependencies": { 5648 - "undici-types": "~5.26.4" 5682 + "undici-types": "~6.21.0" 5649 5683 } 5650 5684 }, 5651 5685 "node_modules/@types/node-forge": { ··· 6424 6458 "license": "Apache-2.0", 6425 6459 "engines": { 6426 6460 "node": ">= 0.4" 6461 + } 6462 + }, 6463 + "node_modules/babel-plugin-react-compiler": { 6464 + "version": "19.1.0-rc.1", 6465 + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.1.0-rc.1.tgz", 6466 + "integrity": "sha512-M4fpG+Hfq5gWzsJeeMErdRokzg0fdJ8IAk+JDhfB/WLT+U3WwJWR8edphypJrk447/JEvYu6DBFwsTn10bMW4Q==", 6467 + "license": "MIT", 6468 + "dependencies": { 6469 + "@babel/types": "^7.26.0" 6427 6470 } 6428 6471 }, 6429 6472 "node_modules/bail": { ··· 15839 15882 } 15840 15883 }, 15841 15884 "node_modules/undici-types": { 15842 - "version": "5.26.5", 15843 - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 15844 - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 15885 + "version": "6.21.0", 15886 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 15887 + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 15888 + "license": "MIT" 15845 15889 }, 15846 15890 "node_modules/unified": { 15847 15891 "version": "11.0.5",
+2
package.json
··· 38 38 "@vercel/analytics": "^1.3.1", 39 39 "@vercel/kv": "^1.0.1", 40 40 "@vercel/sdk": "^1.3.1", 41 + "babel-plugin-react-compiler": "^19.1.0-rc.1", 41 42 "base64-js": "^1.5.1", 42 43 "colorjs.io": "^0.5.2", 43 44 "drizzle-orm": "^0.30.10", ··· 79 80 "@atproto/lex-cli": "^0.6.1", 80 81 "@atproto/lexicon": "^0.4.7", 81 82 "@cloudflare/workers-types": "^4.20240512.0", 83 + "@types/node": "^22.15.17", 82 84 "@types/react": "19.1.3", 83 85 "@types/react-dom": "19.1.3", 84 86 "@types/uuid": "^10.0.0",
src/IdResolver.ts

This is a binary file and will not be displayed.

+1
src/utils/focusBlock.ts
··· 30 30 // focus the editor using the mouse position if needed 31 31 let nextBlockID = block.value; 32 32 let nextBlock = useEditorStates.getState().editorStates[nextBlockID]; 33 + console.log(nextBlock); 33 34 if (!nextBlock || !nextBlock.view) return; 34 35 let nextBlockViewClientRect = nextBlock.view.dom.getBoundingClientRect(); 35 36 let tr = nextBlock.editor.tr;