a tool for shared writing and social publishing

handle pub and post urls in bsky post editor

Changed files
+61 -29
app
components
+31 -14
app/[leaflet_id]/publish/BskyPostEditorProsemirror.tsx
··· 170 170 171 171 const handleMentionSelect = useCallback( 172 172 (mention: Mention) => { 173 - if (mention.type !== "did") return; 174 173 if (!viewRef.current || mentionInsertPos === null) return; 175 174 const view = viewRef.current; 176 175 const from = mentionInsertPos - 1; ··· 180 179 // Delete the @ symbol 181 180 tr.delete(from, to); 182 181 183 - // Insert @handle 184 - const mentionText = "@" + mention.handle; 185 - tr.insertText(mentionText, from); 186 - 187 - // Apply mention mark 188 - tr.addMark( 189 - from, 190 - from + mentionText.length, 191 - bskyPostSchema.marks.mention.create({ did: mention.did }), 192 - ); 193 - 194 - // Add a space after the mention 195 - tr.insertText(" ", from + mentionText.length); 182 + if (mention.type === "did") { 183 + // Insert @handle with mention mark 184 + const mentionText = "@" + mention.handle; 185 + tr.insertText(mentionText, from); 186 + tr.addMark( 187 + from, 188 + from + mentionText.length, 189 + bskyPostSchema.marks.mention.create({ did: mention.did }), 190 + ); 191 + tr.insertText(" ", from + mentionText.length); 192 + } else if (mention.type === "publication") { 193 + // Insert publication name as a link 194 + const linkText = mention.name; 195 + tr.insertText(linkText, from); 196 + tr.addMark( 197 + from, 198 + from + linkText.length, 199 + bskyPostSchema.marks.link.create({ href: mention.url }), 200 + ); 201 + tr.insertText(" ", from + linkText.length); 202 + } else if (mention.type === "post") { 203 + // Insert post title as a link 204 + const linkText = mention.title; 205 + tr.insertText(linkText, from); 206 + tr.addMark( 207 + from, 208 + from + linkText.length, 209 + bskyPostSchema.marks.link.create({ href: mention.url }), 210 + ); 211 + tr.insertText(" ", from + linkText.length); 212 + } 196 213 197 214 view.dispatch(tr); 198 215 view.focus();
+16 -5
app/api/rpc/[command]/search_publication_documents.ts
··· 1 + import { AtUri } from "@atproto/api"; 1 2 import { z } from "zod"; 2 3 import { makeRoute } from "../lib"; 3 4 import type { Env } from "./route"; 5 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 4 6 5 7 export type SearchPublicationDocumentsReturnType = Awaited< 6 8 ReturnType<(typeof search_publication_documents)["handler"]> ··· 18 20 { supabase }: Pick<Env, "supabase">, 19 21 ) => { 20 22 // Get documents in the publication, filtering by title using JSON operator 23 + // Also join with publications to get the record for URL construction 21 24 const { data: documents, error } = await supabase 22 25 .from("documents_in_publications") 23 - .select("document, documents!inner(uri, data)") 26 + .select( 27 + "document, documents!inner(uri, data), publications!inner(uri, record)", 28 + ) 24 29 .eq("publication", publication_uri) 25 30 .ilike("documents.data->>title", `%${query}%`) 26 31 .limit(limit); ··· 31 36 ); 32 37 } 33 38 34 - const result = documents.map((d) => ({ 35 - uri: d.documents.uri, 36 - title: (d.documents.data as { title?: string })?.title || "Untitled", 37 - })); 39 + const result = documents.map((d) => { 40 + const docUri = new AtUri(d.documents.uri); 41 + const pubUrl = getPublicationURL(d.publications); 42 + 43 + return { 44 + uri: d.documents.uri, 45 + title: (d.documents.data as { title?: string })?.title || "Untitled", 46 + url: `${pubUrl}/${docUri.rkey}`, 47 + }; 48 + }); 38 49 39 50 return { result: { documents: result } }; 40 51 },
+10 -8
app/api/rpc/[command]/search_publication_names.ts
··· 1 1 import { z } from "zod"; 2 2 import { makeRoute } from "../lib"; 3 3 import type { Env } from "./route"; 4 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 4 5 5 6 export type SearchPublicationNamesReturnType = Awaited< 6 7 ReturnType<(typeof search_publication_names)["handler"]> ··· 12 13 query: z.string(), 13 14 limit: z.number().optional().default(10), 14 15 }), 15 - handler: async ( 16 - { query, limit }, 17 - { supabase }: Pick<Env, "supabase">, 18 - ) => { 16 + handler: async ({ query, limit }, { supabase }: Pick<Env, "supabase">) => { 19 17 // Search publications by name in record (case-insensitive partial match) 20 18 const { data: publications, error } = await supabase 21 19 .from("publications") ··· 27 25 throw new Error(`Failed to search publications: ${error.message}`); 28 26 } 29 27 30 - const result = publications.map((p) => ({ 31 - uri: p.uri, 32 - name: (p.record as { name?: string })?.name || "Untitled", 33 - })); 28 + const result = publications.map((p) => { 29 + const record = p.record as { name?: string }; 30 + return { 31 + uri: p.uri, 32 + name: record.name || "Untitled", 33 + url: getPublicationURL(p), 34 + }; 35 + }); 34 36 35 37 return { result: { publications: result } }; 36 38 },
+4 -2
components/Mention.tsx
··· 457 457 displayName?: string; 458 458 avatar?: string; 459 459 } 460 - | { type: "publication"; uri: string; name: string } 461 - | { type: "post"; uri: string; title: string }; 460 + | { type: "publication"; uri: string; name: string; url: string } 461 + | { type: "post"; uri: string; title: string; url: string }; 462 462 463 463 export type MentionScope = 464 464 | { type: "default" } ··· 493 493 type: "post" as const, 494 494 uri: d.uri, 495 495 title: d.title, 496 + url: d.url, 496 497 })), 497 498 ); 498 499 } else { ··· 517 518 type: "publication" as const, 518 519 uri: p.uri, 519 520 name: p.name, 521 + url: p.url, 520 522 })), 521 523 ]); 522 524 }