a tool for shared writing and social publishing
0
fork

Configure Feed

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

wire up interactions

+327 -291
+60 -33
app/(home-pages)/reader/InteractionDrawers.tsx
··· 8 8 import { CloseTiny } from "components/Icons/CloseTiny"; 9 9 import { SpeedyLink } from "components/SpeedyLink"; 10 10 import { GoToArrow } from "components/Icons/GoToArrow"; 11 + import { DotLoader } from "components/utils/DotLoader"; 12 + import { ReaderMentionsContent } from "./ReaderMentionsContent"; 13 + import { callRPC } from "app/api/rpc/client"; 14 + import useSWR from "swr"; 15 + import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 11 16 12 17 export const MobileInteractionPreviewDrawer = () => { 13 18 let selectedPost = useSelectedPostListing((s) => s.selectedPostListing); ··· 36 41 const PreviewDrawerContent = (props: { 37 42 selectedPost: SelectedPostListing | null; 38 43 }) => { 39 - if (!props.selectedPost || !props.selectedPost.document) return; 44 + const documentUri = props.selectedPost?.document_uri || null; 45 + const drawer = props.selectedPost?.drawer || null; 46 + 47 + const { data, isLoading } = useSWR( 48 + documentUri ? ["get_document_interactions", documentUri] : null, 49 + async () => { 50 + const res = await callRPC("get_document_interactions", { 51 + document_uri: documentUri!, 52 + }); 53 + return res; 54 + }, 55 + ); 56 + 57 + if (!props.selectedPost || !props.selectedPost.document) return null; 58 + 59 + const postUrl = getDocumentURL( 60 + props.selectedPost.document, 61 + props.selectedPost.document_uri, 62 + ); 63 + 64 + const drawerTitle = 65 + drawer === "quotes" 66 + ? `Mentions of ${props.selectedPost.document.title}` 67 + : `Comments for ${props.selectedPost.document.title}`; 40 68 41 - if (props.selectedPost.drawer === "quotes") { 42 - return ( 43 - <> 44 - {/*<MentionsDrawerContent 45 - did={selectedPost.document_uri} 46 - quotesAndMentions={[]} 47 - />*/} 48 - </> 49 - ); 50 - } else 51 - return ( 52 - <> 53 - <div className="w-full text-sm text-tertiary flex justify-between pt-3 gap-3"> 54 - <div className="truncate min-w-0 grow"> 55 - Comments for {props.selectedPost.document.title} 56 - </div> 57 - <button 58 - className="text-tertiary" 59 - onClick={() => 60 - useSelectedPostListing.getState().setSelectedPostListing(null) 61 - } 62 - > 63 - <CloseTiny /> 64 - </button> 65 - </div> 66 - <SpeedyLink 67 - className="shrink-0 flex gap-1 items-center " 68 - href={"/"} 69 - ></SpeedyLink> 69 + return ( 70 + <> 71 + <div className="w-full text-sm text-tertiary flex justify-between pt-3 gap-3"> 72 + <div className="truncate min-w-0 grow">{drawerTitle}</div> 73 + <button 74 + className="text-tertiary" 75 + onClick={() => 76 + useSelectedPostListing.getState().setSelectedPostListing(null) 77 + } 78 + > 79 + <CloseTiny /> 80 + </button> 81 + </div> 82 + <SpeedyLink className="shrink-0 flex gap-1 items-center" href={postUrl}> 70 83 <ButtonPrimary fullWidth compact className="text-sm! mt-1"> 71 84 See Full Post <GoToArrow /> 72 85 </ButtonPrimary> 86 + </SpeedyLink> 87 + {isLoading ? ( 88 + <div className="flex items-center justify-center gap-1 text-tertiary italic text-sm mt-8"> 89 + <span>loading</span> 90 + <DotLoader /> 91 + </div> 92 + ) : drawer === "quotes" ? ( 93 + <div className="mt-3"> 94 + <ReaderMentionsContent 95 + quotesAndMentions={data?.quotesAndMentions || []} 96 + /> 97 + </div> 98 + ) : ( 73 99 <CommentsDrawerContent 74 100 noCommentBox 75 101 document_uri={props.selectedPost.document_uri} 76 - comments={[]} 102 + comments={data?.comments || []} 77 103 /> 78 - </> 79 - ); 104 + )} 105 + </> 106 + ); 80 107 };
+72
app/(home-pages)/reader/ReaderMentionsContent.tsx
··· 1 + "use client"; 2 + import useSWR from "swr"; 3 + import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 4 + import { BskyPostContent } from "app/lish/[did]/[publication]/[rkey]/BskyPostContent"; 5 + import { DotLoader } from "components/utils/DotLoader"; 6 + 7 + async function fetchBskyPosts(uris: string[]): Promise<PostView[]> { 8 + const params = new URLSearchParams({ 9 + uris: JSON.stringify(uris), 10 + }); 11 + const response = await fetch(`/api/bsky/hydrate?${params.toString()}`); 12 + if (!response.ok) throw new Error("Failed to fetch Bluesky posts"); 13 + return response.json(); 14 + } 15 + 16 + export function ReaderMentionsContent(props: { 17 + quotesAndMentions: { uri: string; link?: string }[]; 18 + }) { 19 + const uris = props.quotesAndMentions.map((q) => q.uri); 20 + const key = uris.length > 0 21 + ? `/api/bsky/hydrate?${new URLSearchParams({ uris: JSON.stringify(uris) }).toString()}` 22 + : null; 23 + 24 + const { data: bskyPosts, isLoading } = useSWR(key, () => 25 + fetchBskyPosts(uris), 26 + ); 27 + 28 + if (props.quotesAndMentions.length === 0) { 29 + return ( 30 + <div className="opaque-container flex flex-col gap-0.5 p-[6px] text-tertiary italic text-sm text-center"> 31 + <div className="font-bold">no mentions yet!</div> 32 + </div> 33 + ); 34 + } 35 + 36 + if (isLoading) { 37 + return ( 38 + <div className="flex items-center justify-center gap-1 text-tertiary italic text-sm mt-8"> 39 + <span>loading</span> 40 + <DotLoader /> 41 + </div> 42 + ); 43 + } 44 + 45 + const postViewMap = new Map<string, PostView>(); 46 + bskyPosts?.forEach((pv) => postViewMap.set(pv.uri, pv)); 47 + 48 + return ( 49 + <div className="flex flex-col gap-4 w-full"> 50 + {props.quotesAndMentions.map((q, index) => { 51 + const post = postViewMap.get(q.uri); 52 + if (!post) return null; 53 + return ( 54 + <div key={q.uri}> 55 + <BskyPostContent 56 + post={post} 57 + parent={undefined} 58 + showBlueskyLink={true} 59 + showEmbed={true} 60 + avatarSize="medium" 61 + className="text-sm" 62 + compactEmbed 63 + /> 64 + {index < props.quotesAndMentions.length - 1 && ( 65 + <hr className="border-border-light mt-4" /> 66 + )} 67 + </div> 68 + ); 69 + })} 70 + </div> 71 + ); 72 + }
+88
app/(home-pages)/reader/enrichPost.ts
··· 1 + import { AtUri } from "@atproto/api"; 2 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 3 + import { getConstellationBacklinks } from "app/lish/[did]/[publication]/[rkey]/getPostPageData"; 4 + import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 5 + import { 6 + normalizeDocumentRecord, 7 + normalizePublicationRecord, 8 + } from "src/utils/normalizeRecords"; 9 + import { idResolver } from "./idResolver"; 10 + import type { Post } from "./getReaderFeed"; 11 + 12 + type RawDocument = { 13 + data: unknown; 14 + uri: string; 15 + sort_date: string; 16 + comments_on_documents: { count: number }[]; 17 + document_mentions_in_bsky: { count: number }[]; 18 + recommends_on_documents: { count: number }[]; 19 + documents_in_publications: { 20 + publications: { 21 + uri: string; 22 + record: unknown; 23 + name: string | null; 24 + [key: string]: unknown; 25 + } | null; 26 + }[]; 27 + }; 28 + 29 + export async function enrichDocumentToPost( 30 + doc: RawDocument, 31 + ): Promise<Post | null> { 32 + const pub = doc.documents_in_publications?.[0]?.publications; 33 + const uri = new AtUri(doc.uri); 34 + const handle = await idResolver.did.resolve(uri.host); 35 + 36 + const normalizedData = normalizeDocumentRecord(doc.data, doc.uri); 37 + if (!normalizedData) return null; 38 + 39 + const normalizedPubRecord = pub 40 + ? normalizePublicationRecord(pub.record) 41 + : null; 42 + 43 + const mentionsCount = await getAccurateMentionsCount( 44 + normalizedData, 45 + doc.uri, 46 + normalizedPubRecord, 47 + doc.document_mentions_in_bsky?.[0]?.count || 0, 48 + ); 49 + 50 + return { 51 + publication: pub 52 + ? { 53 + href: getPublicationURL(pub), 54 + pubRecord: normalizedPubRecord, 55 + uri: pub.uri || "", 56 + } 57 + : undefined, 58 + author: handle?.alsoKnownAs?.[0] 59 + ? `@${handle.alsoKnownAs[0].slice(5)}` 60 + : null, 61 + documents: { 62 + comments_on_documents: doc.comments_on_documents, 63 + document_mentions_in_bsky: doc.document_mentions_in_bsky, 64 + recommends_on_documents: doc.recommends_on_documents, 65 + mentionsCount, 66 + data: normalizedData, 67 + uri: doc.uri, 68 + sort_date: doc.sort_date, 69 + }, 70 + }; 71 + } 72 + 73 + async function getAccurateMentionsCount( 74 + normalizedData: NonNullable<ReturnType<typeof normalizeDocumentRecord>>, 75 + docUri: string, 76 + normalizedPubRecord: ReturnType<typeof normalizePublicationRecord>, 77 + dbMentionsCount: number, 78 + ): Promise<number> { 79 + const postUrl = getDocumentURL(normalizedData, docUri, normalizedPubRecord); 80 + const absoluteUrl = postUrl.startsWith("/") 81 + ? `https://leaflet.pub${postUrl}` 82 + : postUrl; 83 + const constellationBacklinks = await getConstellationBacklinks(absoluteUrl); 84 + const uniqueBacklinkCount = new Set( 85 + constellationBacklinks.map((b) => b.uri), 86 + ).size; 87 + return dbMentionsCount + uniqueBacklinkCount; 88 + }
+2 -40
app/(home-pages)/reader/getHotFeed.ts
··· 5 5 import { pool } from "supabase/pool"; 6 6 import Client from "ioredis"; 7 7 import { AtUri } from "@atproto/api"; 8 - import { idResolver } from "./idResolver"; 9 - import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 10 - import { 11 - normalizeDocumentRecord, 12 - normalizePublicationRecord, 13 - } from "src/utils/normalizeRecords"; 14 8 import { supabaseServerClient } from "supabase/serverClient"; 9 + import { enrichDocumentToPost } from "./enrichPost"; 15 10 import type { Post } from "./getReaderFeed"; 16 11 17 12 let redisClient: Client | null = null; ··· 90 85 // Enrich into Post[] 91 86 const posts = ( 92 87 await Promise.all( 93 - orderedDocs.map(async (doc) => { 94 - const pub = doc.documents_in_publications?.[0]?.publications; 95 - const uri = new AtUri(doc.uri); 96 - const handle = await idResolver.did.resolve(uri.host); 97 - 98 - const normalizedData = normalizeDocumentRecord(doc.data, doc.uri); 99 - if (!normalizedData) return null; 100 - 101 - const normalizedPubRecord = pub 102 - ? normalizePublicationRecord(pub.record) 103 - : null; 104 - 105 - const post: Post = { 106 - publication: pub 107 - ? { 108 - href: getPublicationURL(pub), 109 - pubRecord: normalizedPubRecord, 110 - uri: pub.uri || "", 111 - } 112 - : undefined, 113 - author: handle?.alsoKnownAs?.[0] 114 - ? `@${handle.alsoKnownAs[0].slice(5)}` 115 - : null, 116 - documents: { 117 - comments_on_documents: doc.comments_on_documents, 118 - document_mentions_in_bsky: doc.document_mentions_in_bsky, 119 - recommends_on_documents: doc.recommends_on_documents, 120 - data: normalizedData, 121 - uri: doc.uri, 122 - sort_date: doc.sort_date, 123 - }, 124 - }; 125 - return post; 126 - }), 88 + orderedDocs.map((doc) => enrichDocumentToPost(doc as any)), 127 89 ) 128 90 ).filter((post): post is Post => post !== null); 129 91
+2 -39
app/(home-pages)/reader/getNewFeed.ts
··· 1 1 "use server"; 2 2 3 - import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 4 3 import { supabaseServerClient } from "supabase/serverClient"; 5 - import { AtUri } from "@atproto/api"; 6 - import { idResolver } from "./idResolver"; 7 - import { 8 - normalizeDocumentRecord, 9 - normalizePublicationRecord, 10 - } from "src/utils/normalizeRecords"; 11 4 import { deduplicateByUriOrdered } from "src/utils/deduplicateRecords"; 5 + import { enrichDocumentToPost } from "./enrichPost"; 12 6 import type { Cursor, Post } from "./getReaderFeed"; 13 7 14 8 export async function getNewFeed( ··· 42 36 const feed = deduplicateByUriOrdered(rawFeed || []); 43 37 44 38 let posts = ( 45 - await Promise.all( 46 - feed.map(async (post) => { 47 - let pub = post.documents_in_publications[0]?.publications!; 48 - let uri = new AtUri(post.uri); 49 - let handle = await idResolver.did.resolve(uri.host); 50 - 51 - const normalizedData = normalizeDocumentRecord(post.data, post.uri); 52 - if (!normalizedData) return null; 53 - 54 - const normalizedPubRecord = normalizePublicationRecord(pub?.record); 55 - 56 - let p: Post = { 57 - publication: { 58 - href: getPublicationURL(pub), 59 - pubRecord: normalizedPubRecord, 60 - uri: pub?.uri || "", 61 - }, 62 - author: handle?.alsoKnownAs?.[0] 63 - ? `@${handle.alsoKnownAs[0].slice(5)}` 64 - : null, 65 - documents: { 66 - comments_on_documents: post.comments_on_documents, 67 - document_mentions_in_bsky: post.document_mentions_in_bsky, 68 - recommends_on_documents: post.recommends_on_documents, 69 - data: normalizedData, 70 - uri: post.uri, 71 - sort_date: post.sort_date, 72 - }, 73 - }; 74 - return p; 75 - }) || [], 76 - ) 39 + await Promise.all(feed.map((post) => enrichDocumentToPost(post as any))) 77 40 ).filter((post): post is Post => post !== null); 78 41 79 42 const nextCursor =
+7 -44
app/(home-pages)/reader/getReaderFeed.ts
··· 1 1 "use server"; 2 2 3 3 import { getIdentityData } from "actions/getIdentityData"; 4 - import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 5 4 import { supabaseServerClient } from "supabase/serverClient"; 6 - import { IdResolver } from "@atproto/identity"; 7 - import type { DidCache, CacheResult, DidDocument } from "@atproto/identity"; 8 - import Client from "ioredis"; 9 - import { AtUri } from "@atproto/api"; 10 - import { idResolver } from "./idResolver"; 11 - import { 12 - normalizeDocumentRecord, 13 - normalizePublicationRecord, 14 - type NormalizedDocument, 15 - type NormalizedPublication, 5 + import type { 6 + NormalizedDocument, 7 + NormalizedPublication, 16 8 } from "src/utils/normalizeRecords"; 17 9 import { deduplicateByUriOrdered } from "src/utils/deduplicateRecords"; 10 + import { enrichDocumentToPost } from "./enrichPost"; 18 11 19 12 export type Cursor = { 20 13 timestamp: string; ··· 53 46 const feed = deduplicateByUriOrdered(rawFeed || []); 54 47 55 48 let posts = ( 56 - await Promise.all( 57 - feed.map(async (post) => { 58 - let pub = post.documents_in_publications[0].publications!; 59 - let uri = new AtUri(post.uri); 60 - let handle = await idResolver.did.resolve(uri.host); 61 - 62 - // Normalize records - filter out unrecognized formats 63 - const normalizedData = normalizeDocumentRecord(post.data, post.uri); 64 - if (!normalizedData) return null; 49 + await Promise.all(feed.map((post) => enrichDocumentToPost(post as any))) 50 + ).filter((post): post is Post => post !== null); 65 51 66 - const normalizedPubRecord = normalizePublicationRecord(pub?.record); 67 - 68 - let p: Post = { 69 - publication: { 70 - href: getPublicationURL(pub), 71 - pubRecord: normalizedPubRecord, 72 - uri: pub?.uri || "", 73 - }, 74 - author: handle?.alsoKnownAs?.[0] 75 - ? `@${handle.alsoKnownAs[0].slice(5)}` 76 - : null, 77 - documents: { 78 - comments_on_documents: post.comments_on_documents, 79 - document_mentions_in_bsky: post.document_mentions_in_bsky, 80 - recommends_on_documents: post.recommends_on_documents, 81 - data: normalizedData, 82 - uri: post.uri, 83 - sort_date: post.sort_date, 84 - }, 85 - }; 86 - return p; 87 - }) || [], 88 - ) 89 - ).filter((post): post is Post => post !== null); 90 52 const nextCursor = 91 53 posts.length > 0 92 54 ? { ··· 115 77 comments_on_documents: { count: number }[] | undefined; 116 78 document_mentions_in_bsky: { count: number }[] | undefined; 117 79 recommends_on_documents: { count: number }[] | undefined; 80 + mentionsCount?: number; 118 81 }; 119 82 };
+90
app/api/rpc/[command]/get_document_interactions.ts
··· 1 + import { z } from "zod"; 2 + import { makeRoute } from "../lib"; 3 + import type { Env } from "./route"; 4 + import { getConstellationBacklinks } from "app/lish/[did]/[publication]/[rkey]/getPostPageData"; 5 + import { getDocumentURL } from "app/lish/createPub/getPublicationURL"; 6 + import { 7 + normalizeDocumentRecord, 8 + normalizePublicationRecord, 9 + } from "src/utils/normalizeRecords"; 10 + 11 + export const get_document_interactions = makeRoute({ 12 + route: "get_document_interactions", 13 + input: z.object({ 14 + document_uri: z.string(), 15 + }), 16 + handler: async ( 17 + { document_uri }, 18 + { supabase }: Pick<Env, "supabase">, 19 + ) => { 20 + let { data: document } = await supabase 21 + .from("documents") 22 + .select( 23 + ` 24 + data, 25 + uri, 26 + comments_on_documents(*, bsky_profiles(*)), 27 + document_mentions_in_bsky(*), 28 + documents_in_publications(publications(*)) 29 + `, 30 + ) 31 + .eq("uri", document_uri) 32 + .limit(1) 33 + .single(); 34 + 35 + if (!document) { 36 + return { comments: [], quotesAndMentions: [], totalMentionsCount: 0 }; 37 + } 38 + 39 + const normalizedData = normalizeDocumentRecord( 40 + document.data, 41 + document.uri, 42 + ); 43 + 44 + const pub = document.documents_in_publications?.[0]?.publications; 45 + const normalizedPubRecord = pub 46 + ? normalizePublicationRecord(pub.record) 47 + : null; 48 + 49 + // Compute document URL for constellation lookup 50 + let absoluteUrl = ""; 51 + if (normalizedData) { 52 + const postUrl = getDocumentURL( 53 + normalizedData, 54 + document.uri, 55 + normalizedPubRecord, 56 + ); 57 + absoluteUrl = postUrl.startsWith("/") 58 + ? `https://leaflet.pub${postUrl}` 59 + : postUrl; 60 + } 61 + 62 + // Fetch constellation backlinks 63 + const constellationBacklinks = absoluteUrl 64 + ? await getConstellationBacklinks(absoluteUrl) 65 + : []; 66 + 67 + // Deduplicate constellation backlinks internally 68 + const uniqueBacklinks = Array.from( 69 + new Map(constellationBacklinks.map((b) => [b.uri, b])).values(), 70 + ); 71 + 72 + // Combine DB mentions and constellation backlinks, deduplicating by URI 73 + const dbMentionUris = new Set( 74 + document.document_mentions_in_bsky.map((m) => m.uri), 75 + ); 76 + const quotesAndMentions: { uri: string; link?: string }[] = [ 77 + ...document.document_mentions_in_bsky.map((m) => ({ 78 + uri: m.uri, 79 + link: m.link, 80 + })), 81 + ...uniqueBacklinks.filter((b) => !dbMentionUris.has(b.uri)), 82 + ]; 83 + 84 + return { 85 + comments: document.comments_on_documents, 86 + quotesAndMentions, 87 + totalMentionsCount: quotesAndMentions.length, 88 + }; 89 + }, 90 + });
+3 -134
app/api/rpc/[command]/get_hot_feed.ts
··· 1 1 import { z } from "zod"; 2 2 import { makeRoute } from "../lib"; 3 3 import type { Env } from "./route"; 4 - import { drizzle } from "drizzle-orm/node-postgres"; 5 - import { sql } from "drizzle-orm"; 6 - import { pool } from "supabase/pool"; 7 - import Client from "ioredis"; 8 - import { AtUri } from "@atproto/api"; 9 - import { idResolver } from "app/(home-pages)/reader/idResolver"; 10 - import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 11 - import { 12 - normalizeDocumentRecord, 13 - normalizePublicationRecord, 14 - } from "src/utils/normalizeRecords"; 4 + import { getHotFeed } from "app/(home-pages)/reader/getHotFeed"; 15 5 import type { Post } from "app/(home-pages)/reader/getReaderFeed"; 16 6 17 - let redisClient: Client | null = null; 18 - if (process.env.REDIS_URL && process.env.NODE_ENV === "production") { 19 - redisClient = new Client(process.env.REDIS_URL); 20 - } 21 - 22 - const CACHE_KEY = "hot_feed_v1"; 23 - const CACHE_TTL = 300; // 5 minutes 24 - 25 7 export type GetHotFeedReturnType = Awaited< 26 8 ReturnType<(typeof get_hot_feed)["handler"]> 27 9 >; ··· 29 11 export const get_hot_feed = makeRoute({ 30 12 route: "get_hot_feed", 31 13 input: z.object({}), 32 - handler: async ({}, { supabase }: Pick<Env, "supabase">) => { 33 - // Check Redis cache 34 - if (redisClient) { 35 - const cached = await redisClient.get(CACHE_KEY); 36 - if (cached) { 37 - return JSON.parse(cached) as { posts: Post[] }; 38 - } 39 - } 40 - 41 - // Run ranked SQL query to get top 50 URIs 42 - const client = await pool.connect(); 43 - const db = drizzle(client); 44 - 45 - let uris: string[]; 46 - try { 47 - const ranked = await db.execute(sql` 48 - SELECT uri 49 - FROM documents 50 - WHERE indexed = true 51 - AND sort_date > now() - interval '7 days' 52 - ORDER BY 53 - (bsky_like_count + recommend_count * 5)::numeric 54 - / power(extract(epoch from (now() - sort_date)) / 3600 + 2, 1.5) DESC 55 - LIMIT 50 56 - `); 57 - uris = ranked.rows.map((row: any) => row.uri as string); 58 - } finally { 59 - client.release(); 60 - } 61 - 62 - if (uris.length === 0) { 63 - return { posts: [] as Post[] }; 64 - } 65 - 66 - // Batch-fetch documents with publication joins and interaction counts 67 - const { data: documents } = await supabase 68 - .from("documents") 69 - .select( 70 - `*, 71 - comments_on_documents(count), 72 - document_mentions_in_bsky(count), 73 - recommends_on_documents(count), 74 - documents_in_publications(publications(*))`, 75 - ) 76 - .in("uri", uris); 77 - 78 - // Build lookup map for enrichment 79 - const docMap = new Map( 80 - (documents || []).map((d) => [d.uri, d]), 81 - ); 82 - 83 - // Process in ranked order, deduplicating by identity key (DID/rkey) 84 - const seen = new Set<string>(); 85 - const orderedDocs: (typeof documents extends (infer T)[] | null ? T : never)[] = []; 86 - for (const uri of uris) { 87 - try { 88 - const parsed = new AtUri(uri); 89 - const identityKey = `${parsed.host}/${parsed.rkey}`; 90 - if (seen.has(identityKey)) continue; 91 - seen.add(identityKey); 92 - } catch { 93 - // invalid URI, skip dedup check 94 - } 95 - const doc = docMap.get(uri); 96 - if (doc) orderedDocs.push(doc); 97 - } 98 - 99 - // Enrich into Post[] 100 - const posts = ( 101 - await Promise.all( 102 - orderedDocs.map(async (doc) => { 103 - const pub = doc.documents_in_publications?.[0]?.publications; 104 - const uri = new AtUri(doc.uri); 105 - const handle = await idResolver.did.resolve(uri.host); 106 - 107 - const normalizedData = normalizeDocumentRecord(doc.data, doc.uri); 108 - if (!normalizedData) return null; 109 - 110 - const normalizedPubRecord = pub 111 - ? normalizePublicationRecord(pub.record) 112 - : null; 113 - 114 - const post: Post = { 115 - publication: pub 116 - ? { 117 - href: getPublicationURL(pub), 118 - pubRecord: normalizedPubRecord, 119 - uri: pub.uri || "", 120 - } 121 - : undefined, 122 - author: handle?.alsoKnownAs?.[0] 123 - ? `@${handle.alsoKnownAs[0].slice(5)}` 124 - : null, 125 - documents: { 126 - comments_on_documents: doc.comments_on_documents, 127 - document_mentions_in_bsky: doc.document_mentions_in_bsky, 128 - recommends_on_documents: doc.recommends_on_documents, 129 - data: normalizedData, 130 - uri: doc.uri, 131 - sort_date: doc.sort_date, 132 - }, 133 - }; 134 - return post; 135 - }), 136 - ) 137 - ).filter((post): post is Post => post !== null); 138 - 139 - const response = { posts }; 140 - 141 - // Cache in Redis 142 - if (redisClient) { 143 - await redisClient.setex(CACHE_KEY, CACHE_TTL, JSON.stringify(response)); 144 - } 145 - 146 - return response; 14 + handler: async ({}, {}: Pick<Env, "supabase">) => { 15 + return await getHotFeed(); 147 16 }, 148 17 });
+2
app/api/rpc/[command]/route.ts
··· 16 16 import { get_profile_data } from "./get_profile_data"; 17 17 import { get_user_recommendations } from "./get_user_recommendations"; 18 18 import { get_hot_feed } from "./get_hot_feed"; 19 + import { get_document_interactions } from "./get_document_interactions"; 19 20 20 21 let supabase = createClient<Database>( 21 22 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, ··· 45 46 get_profile_data, 46 47 get_user_recommendations, 47 48 get_hot_feed, 49 + get_document_interactions, 48 50 ]; 49 51 export async function POST( 50 52 req: Request,
+1 -1
components/PostListing.tsx
··· 74 74 pubRecord?.preferences, 75 75 ); 76 76 77 - let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0; 77 + let quotes = props.documents.mentionsCount ?? props.documents.document_mentions_in_bsky?.[0]?.count ?? 0; 78 78 let comments = 79 79 mergedPrefs.showComments === false 80 80 ? 0