a tool for shared writing and social publishing
0
fork

Configure Feed

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

Profile popover init

celine c2395286 53926c04

+120 -98
+1
app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx
··· 46 46 {/*<div className="spacer "/>*/} 47 47 {props.publications.map((p) => ( 48 48 <PublicationCard 49 + key={p.uri} 49 50 record={p.record as PubLeafletPublication.Record} 50 51 uri={p.uri} 51 52 />
+61
app/api/rpc/[command]/get_profile_data.ts
··· 1 + import { z } from "zod"; 2 + import { makeRoute } from "../lib"; 3 + import type { Env } from "./route"; 4 + import { idResolver } from "app/(home-pages)/reader/idResolver"; 5 + 6 + export type GetProfileDataReturnType = Awaited< 7 + ReturnType<(typeof get_profile_data)["handler"]> 8 + >; 9 + 10 + export const get_profile_data = makeRoute({ 11 + route: "get_profile_data", 12 + input: z.object({ 13 + didOrHandle: z.string(), 14 + }), 15 + handler: async ({ didOrHandle }, { supabase }: Pick<Env, "supabase">) => { 16 + // Resolve handle to DID if necessary 17 + let did = didOrHandle; 18 + 19 + if (!didOrHandle.startsWith("did:")) { 20 + const resolved = await idResolver.handle.resolve(didOrHandle); 21 + if (!resolved) { 22 + throw new Error("Could not resolve handle to DID"); 23 + } 24 + did = resolved; 25 + } 26 + 27 + // Fetch profile 28 + const { data: profile, error: profileError } = await supabase 29 + .from("bsky_profiles") 30 + .select("*") 31 + .eq("did", did) 32 + .single(); 33 + 34 + if (profileError) { 35 + throw new Error(`Failed to fetch profile: ${profileError.message}`); 36 + } 37 + 38 + if (!profile) { 39 + throw new Error("Profile not found"); 40 + } 41 + 42 + // Fetch publications for the DID 43 + const { data: publications, error: publicationsError } = await supabase 44 + .from("publications") 45 + .select("*") 46 + .eq("identity_did", did); 47 + 48 + if (publicationsError) { 49 + throw new Error( 50 + `Failed to fetch publications: ${publicationsError.message}`, 51 + ); 52 + } 53 + 54 + return { 55 + result: { 56 + profile, 57 + publications: publications || [], 58 + }, 59 + }; 60 + }, 61 + });
+2
app/api/rpc/[command]/route.ts
··· 13 13 import { get_publication_data } from "./get_publication_data"; 14 14 import { search_publication_names } from "./search_publication_names"; 15 15 import { search_publication_documents } from "./search_publication_documents"; 16 + import { get_profile_data } from "./get_profile_data"; 16 17 17 18 let supabase = createClient<Database>( 18 19 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, ··· 39 40 get_publication_data, 40 41 search_publication_names, 41 42 search_publication_documents, 43 + get_profile_data, 42 44 ]; 43 45 export async function POST( 44 46 req: Request,
+14 -98
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
··· 18 18 import { QuoteContent } from "../Quotes"; 19 19 import { timeAgo } from "src/utils/timeAgo"; 20 20 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 21 + import { ProfilePopover } from "components/ProfilePopover"; 21 22 22 23 export type Comment = { 23 24 record: Json; 24 25 uri: string; 25 - bsky_profiles: { record: Json } | null; 26 + bsky_profiles: { record: Json; did: string } | null; 26 27 }; 27 28 export function Comments(props: { 28 29 document_uri: string; ··· 109 110 document: string; 110 111 comment: Comment; 111 112 comments: Comment[]; 112 - profile?: AppBskyActorProfile.Record; 113 + profile: AppBskyActorProfile.Record; 113 114 record: PubLeafletComment.Record; 114 115 pageId?: string; 115 116 }) => { 117 + const did = props.comment.bsky_profiles?.did; 118 + 116 119 return ( 117 120 <div id={props.comment.uri} className="comment"> 118 121 <div className="flex gap-2"> 119 - {props.profile && ( 120 - <ProfilePopover profile={props.profile} comment={props.comment.uri} /> 122 + {did && ( 123 + <ProfilePopover 124 + didOrHandle={did} 125 + trigger={ 126 + <div className="text-sm text-tertiary font-bold hover:underline"> 127 + {props.profile.displayName} 128 + </div> 129 + } 130 + /> 121 131 )} 122 - <DatePopover date={props.record.createdAt} /> 123 132 </div> 124 133 {props.record.attachment && 125 134 PubLeafletComment.isLinearDocumentQuote(props.record.attachment) && ( ··· 291 300 </Popover> 292 301 ); 293 302 }; 294 - 295 - const ProfilePopover = (props: { 296 - profile: AppBskyActorProfile.Record; 297 - comment: string; 298 - }) => { 299 - let commenterId = new AtUri(props.comment).host; 300 - 301 - return ( 302 - <> 303 - <a 304 - className="font-bold text-tertiary text-sm hover:underline" 305 - href={`https://bsky.app/profile/${commenterId}`} 306 - > 307 - {props.profile.displayName} 308 - </a> 309 - {/*<Media mobile={false}> 310 - <Popover 311 - align="start" 312 - trigger={ 313 - <div 314 - onMouseOver={() => { 315 - setHovering(true); 316 - hoverTimeout.current = window.setTimeout(() => { 317 - setLoadProfile(true); 318 - }, 500); 319 - }} 320 - onMouseOut={() => { 321 - setHovering(false); 322 - clearTimeout(hoverTimeout.current); 323 - }} 324 - className="font-bold text-tertiary text-sm hover:underline" 325 - > 326 - {props.profile.displayName} 327 - </div> 328 - } 329 - className="max-w-sm" 330 - > 331 - {profile && ( 332 - <> 333 - <div className="profilePopover text-sm flex gap-2"> 334 - <div className="w-5 h-5 bg-test rounded-full shrink-0 mt-[2px]" /> 335 - <div className="flex flex-col"> 336 - <div className="flex justify-between"> 337 - <div className="profileHeader flex gap-2 items-center"> 338 - <div className="font-bold">celine</div> 339 - <a className="text-tertiary" href="/"> 340 - @{profile.handle} 341 - </a> 342 - </div> 343 - </div> 344 - 345 - <div className="profileBio text-secondary "> 346 - {profile.description} 347 - </div> 348 - <div className="flex flex-row gap-2 items-center pt-2 font-bold"> 349 - {!profile.viewer?.following ? ( 350 - <div className="text-tertiary bg-border-light rounded-md px-1 py-0"> 351 - Following 352 - </div> 353 - ) : ( 354 - <ButtonPrimary compact className="text-sm"> 355 - Follow <BlueskyTiny /> 356 - </ButtonPrimary> 357 - )} 358 - {profile.viewer?.followedBy && ( 359 - <div className="text-tertiary">Follows You</div> 360 - )} 361 - </div> 362 - </div> 363 - </div> 364 - 365 - <hr className="my-2 border-border-light" /> 366 - <div className="flex gap-2 leading-tight items-center text-tertiary text-sm"> 367 - <div className="flex flex-col w-6 justify-center"> 368 - {profile.viewer?.knownFollowers?.followers.map((follower) => { 369 - return ( 370 - <div 371 - className="w-[18px] h-[18px] bg-test rounded-full border-2 border-bg-page" 372 - key={follower.did} 373 - /> 374 - ); 375 - })} 376 - <div className="w-[18px] h-[18px] bg-test rounded-full -mt-2 border-2 border-bg-page" /> 377 - <div className="w-[18px] h-[18px] bg-test rounded-full -mt-2 border-2 border-bg-page" /> 378 - </div> 379 - </div> 380 - </> 381 - )} 382 - </Popover> 383 - </Media>*/} 384 - </> 385 - ); 386 - };
+42
components/ProfilePopover.tsx
··· 1 + "use client"; 2 + import { ProfileHeader } from "app/p/[didOrHandle]/(profile)/ProfileHeader"; 3 + import { Popover } from "./Popover"; 4 + import useSWR from "swr"; 5 + import { callRPC } from "app/api/rpc/client"; 6 + import { useState } from "react"; 7 + 8 + export const ProfilePopover = (props: { 9 + trigger: React.ReactNode; 10 + didOrHandle: string; 11 + }) => { 12 + const [isOpen, setIsOpen] = useState(false); 13 + 14 + const { data, isLoading } = useSWR( 15 + isOpen ? ["profile-data", props.didOrHandle] : null, 16 + async () => { 17 + const response = await callRPC("get_profile_data", { 18 + didOrHandle: props.didOrHandle, 19 + }); 20 + return response.result; 21 + }, 22 + ); 23 + 24 + return ( 25 + <Popover 26 + className="max-w-sm p-0! text-center" 27 + trigger={props.trigger} 28 + onOpenChange={setIsOpen} 29 + > 30 + {isLoading ? ( 31 + <div className="text-secondary p-4">Loading...</div> 32 + ) : data ? ( 33 + <ProfileHeader 34 + profile={data.profile} 35 + publications={data.publications} 36 + /> 37 + ) : ( 38 + <div className="text-secondary p-4">Profile not found</div> 39 + )} 40 + </Popover> 41 + ); 42 + };