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 171 const handleMentionSelect = useCallback( 172 (mention: Mention) => { 173 - if (mention.type !== "did") return; 174 if (!viewRef.current || mentionInsertPos === null) return; 175 const view = viewRef.current; 176 const from = mentionInsertPos - 1; ··· 180 // Delete the @ symbol 181 tr.delete(from, to); 182 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); 196 197 view.dispatch(tr); 198 view.focus();
··· 170 171 const handleMentionSelect = useCallback( 172 (mention: Mention) => { 173 if (!viewRef.current || mentionInsertPos === null) return; 174 const view = viewRef.current; 175 const from = mentionInsertPos - 1; ··· 179 // Delete the @ symbol 180 tr.delete(from, to); 181 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 + } 213 214 view.dispatch(tr); 215 view.focus();
+16 -5
app/api/rpc/[command]/search_publication_documents.ts
··· 1 import { z } from "zod"; 2 import { makeRoute } from "../lib"; 3 import type { Env } from "./route"; 4 5 export type SearchPublicationDocumentsReturnType = Awaited< 6 ReturnType<(typeof search_publication_documents)["handler"]> ··· 18 { supabase }: Pick<Env, "supabase">, 19 ) => { 20 // Get documents in the publication, filtering by title using JSON operator 21 const { data: documents, error } = await supabase 22 .from("documents_in_publications") 23 - .select("document, documents!inner(uri, data)") 24 .eq("publication", publication_uri) 25 .ilike("documents.data->>title", `%${query}%`) 26 .limit(limit); ··· 31 ); 32 } 33 34 - const result = documents.map((d) => ({ 35 - uri: d.documents.uri, 36 - title: (d.documents.data as { title?: string })?.title || "Untitled", 37 - })); 38 39 return { result: { documents: result } }; 40 },
··· 1 + import { AtUri } from "@atproto/api"; 2 import { z } from "zod"; 3 import { makeRoute } from "../lib"; 4 import type { Env } from "./route"; 5 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 6 7 export type SearchPublicationDocumentsReturnType = Awaited< 8 ReturnType<(typeof search_publication_documents)["handler"]> ··· 20 { supabase }: Pick<Env, "supabase">, 21 ) => { 22 // Get documents in the publication, filtering by title using JSON operator 23 + // Also join with publications to get the record for URL construction 24 const { data: documents, error } = await supabase 25 .from("documents_in_publications") 26 + .select( 27 + "document, documents!inner(uri, data), publications!inner(uri, record)", 28 + ) 29 .eq("publication", publication_uri) 30 .ilike("documents.data->>title", `%${query}%`) 31 .limit(limit); ··· 36 ); 37 } 38 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 + }); 49 50 return { result: { documents: result } }; 51 },
+10 -8
app/api/rpc/[command]/search_publication_names.ts
··· 1 import { z } from "zod"; 2 import { makeRoute } from "../lib"; 3 import type { Env } from "./route"; 4 5 export type SearchPublicationNamesReturnType = Awaited< 6 ReturnType<(typeof search_publication_names)["handler"]> ··· 12 query: z.string(), 13 limit: z.number().optional().default(10), 14 }), 15 - handler: async ( 16 - { query, limit }, 17 - { supabase }: Pick<Env, "supabase">, 18 - ) => { 19 // Search publications by name in record (case-insensitive partial match) 20 const { data: publications, error } = await supabase 21 .from("publications") ··· 27 throw new Error(`Failed to search publications: ${error.message}`); 28 } 29 30 - const result = publications.map((p) => ({ 31 - uri: p.uri, 32 - name: (p.record as { name?: string })?.name || "Untitled", 33 - })); 34 35 return { result: { publications: result } }; 36 },
··· 1 import { z } from "zod"; 2 import { makeRoute } from "../lib"; 3 import type { Env } from "./route"; 4 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 5 6 export type SearchPublicationNamesReturnType = Awaited< 7 ReturnType<(typeof search_publication_names)["handler"]> ··· 13 query: z.string(), 14 limit: z.number().optional().default(10), 15 }), 16 + handler: async ({ query, limit }, { supabase }: Pick<Env, "supabase">) => { 17 // Search publications by name in record (case-insensitive partial match) 18 const { data: publications, error } = await supabase 19 .from("publications") ··· 25 throw new Error(`Failed to search publications: ${error.message}`); 26 } 27 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 + }); 36 37 return { result: { publications: result } }; 38 },
+4 -2
components/Mention.tsx
··· 457 displayName?: string; 458 avatar?: string; 459 } 460 - | { type: "publication"; uri: string; name: string } 461 - | { type: "post"; uri: string; title: string }; 462 463 export type MentionScope = 464 | { type: "default" } ··· 493 type: "post" as const, 494 uri: d.uri, 495 title: d.title, 496 })), 497 ); 498 } else { ··· 517 type: "publication" as const, 518 uri: p.uri, 519 name: p.name, 520 })), 521 ]); 522 }
··· 457 displayName?: string; 458 avatar?: string; 459 } 460 + | { type: "publication"; uri: string; name: string; url: string } 461 + | { type: "post"; uri: string; title: string; url: string }; 462 463 export type MentionScope = 464 | { type: "default" } ··· 493 type: "post" as const, 494 uri: d.uri, 495 title: d.title, 496 + url: d.url, 497 })), 498 ); 499 } else { ··· 518 type: "publication" as const, 519 uri: p.uri, 520 name: p.name, 521 + url: p.url, 522 })), 523 ]); 524 }