👁️
5
fork

Configure Feed

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

show bsky relationship, change profile layout

+102 -10
+9 -9
src/components/profile/ProfileHeader.tsx
··· 1 1 import { ExternalLink, Pencil } from "lucide-react"; 2 2 import { useCallback, useId, useMemo, useState } from "react"; 3 + import { RelationshipBadge } from "@/components/profile/RelationshipBadge"; 3 4 import { ProseMirrorEditor } from "@/components/richtext/ProseMirrorEditor"; 4 5 import { RichtextRenderer } from "@/components/richtext/RichtextRenderer"; 5 6 import { schema } from "@/components/richtext/schema"; ··· 166 167 {/* Handle row */} 167 168 <div className="flex items-center gap-3"> 168 169 <h1 169 - className="text-3xl font-semibold text-gray-900 dark:text-white" 170 + className="text-2xl sm:text-3xl md:text-4xl font-semibold text-gray-900 dark:text-white truncate min-w-0 tracking-tight sm:tracking-normal" 170 171 style={{ fontVariationSettings: "'MONO' 0.5, 'CASL' 0.3" }} 171 172 > 172 173 {displayHandle} ··· 176 177 href={handleUrl} 177 178 target="_blank" 178 179 rel="noopener noreferrer" 179 - className="text-gray-400 hover:text-cyan-500 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors" 180 + className="shrink-0 text-gray-400 hover:text-cyan-500 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors" 180 181 title={`Visit ${handleUrl}`} 181 182 > 182 - <ExternalLink className="w-6 h-6" /> 183 + <ExternalLink className="w-5 h-5 sm:w-6 sm:h-6" /> 183 184 </a> 184 185 )} 185 186 </div> 186 - {/* Pronouns */} 187 - {profile?.pronouns && ( 188 - <p className="text-sm text-gray-500 dark:text-zinc-300"> 189 - {profile.pronouns} 190 - </p> 191 - )} 187 + {/* Pronouns + relationship */} 188 + <div className="flex items-center gap-2 text-sm text-gray-500 dark:text-zinc-300"> 189 + {profile?.pronouns && <span>{profile.pronouns}</span>} 190 + <RelationshipBadge profileDid={did} /> 191 + </div> 192 192 193 193 {/* Bio */} 194 194 {hasContent && profile?.bio && (
+1 -1
src/components/profile/ProfileLayout.tsx
··· 63 63 64 64 return ( 65 65 <div className="min-h-screen bg-white dark:bg-zinc-900"> 66 - <div className="max-w-7xl mx-auto px-6 py-16"> 66 + <div className="max-w-7xl mx-auto px-4 sm:px-6 py-8 sm:py-14 md:py-16"> 67 67 {/* Profile Header */} 68 68 <ProfileHeader 69 69 profile={profileData?.profile ?? null}
+92
src/components/profile/RelationshipBadge.tsx
··· 1 + import { Client, ok, simpleFetchHandler } from "@atcute/client"; 2 + import type { Did } from "@atcute/lexicons"; 3 + import { queryOptions, useQuery } from "@tanstack/react-query"; 4 + import { useAuth } from "@/lib/useAuth"; 5 + 6 + interface FollowRelationship { 7 + viewerFollows: boolean; 8 + profileFollows: boolean; 9 + } 10 + 11 + const handler = simpleFetchHandler({ service: "https://public.api.bsky.app" }); 12 + const rpc = new Client({ handler }); 13 + 14 + async function getRelationship( 15 + viewerDid: string, 16 + profileDid: string, 17 + ): Promise<FollowRelationship> { 18 + const data = await ok( 19 + rpc.get("app.bsky.graph.getRelationships", { 20 + params: { 21 + actor: viewerDid as Did, 22 + others: [profileDid as Did], 23 + }, 24 + }), 25 + ); 26 + 27 + if (data.relationships.length === 0) { 28 + return { viewerFollows: false, profileFollows: false }; 29 + } 30 + 31 + const rel = data.relationships[0]; 32 + if (rel.$type === "app.bsky.graph.defs#notFoundActor") { 33 + return { viewerFollows: false, profileFollows: false }; 34 + } 35 + 36 + return { 37 + viewerFollows: !!rel.following, 38 + profileFollows: !!rel.followedBy, 39 + }; 40 + } 41 + 42 + const relationshipQueryOptions = (viewerDid: string, profileDid: string) => 43 + queryOptions({ 44 + queryKey: ["bsky-relationship", viewerDid, profileDid] as const, 45 + queryFn: () => getRelationship(viewerDid, profileDid), 46 + staleTime: 5 * 60 * 1000, 47 + enabled: !!viewerDid && !!profileDid && viewerDid !== profileDid, 48 + }); 49 + 50 + interface RelationshipBadgeProps { 51 + profileDid: string; 52 + } 53 + 54 + export function RelationshipBadge({ profileDid }: RelationshipBadgeProps) { 55 + const { session } = useAuth(); 56 + const viewerDid = session?.info.sub ?? null; 57 + 58 + const { data } = useQuery( 59 + relationshipQueryOptions(viewerDid ?? "", profileDid), 60 + ); 61 + 62 + if (!viewerDid || viewerDid === profileDid || !data) return null; 63 + 64 + const { viewerFollows, profileFollows } = data; 65 + if (!viewerFollows && !profileFollows) return null; 66 + 67 + const label = 68 + viewerFollows && profileFollows 69 + ? "mutuals" 70 + : profileFollows 71 + ? "follows you" 72 + : "following"; 73 + 74 + return ( 75 + <span 76 + className="inline-flex items-baseline gap-1 px-2 py-0.5 text-xs rounded-full bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 border border-zinc-200 dark:border-zinc-700 shrink-0" 77 + style={{ fontVariationSettings: "'CASL' 0.5" }} 78 + > 79 + <svg 80 + viewBox="0 0 320 286" 81 + fill="currentColor" 82 + className="w-3 h-3 self-center opacity-50" 83 + role="img" 84 + aria-label="Bluesky" 85 + > 86 + <title>Bluesky</title> 87 + <path d="M69.364 19.146c36.687 27.806 76.147 84.186 90.636 114.439 14.489-30.253 53.948-86.633 90.636-114.439C277.107-.917 320-16.44 320 32.957c0 9.865-5.603 82.875-8.889 94.729-11.423 41.208-53.045 51.719-90.071 45.357 64.719 11.12 81.182 47.953 45.627 84.785-80 82.874-106.667-44.333-106.667-44.333s-26.667 127.207-106.667 44.333c-35.555-36.832-19.092-73.665 45.627-84.785-37.026 6.362-78.648-4.149-90.071-45.357C5.603 115.832 0 42.822 0 32.957 0-16.44 42.893-.917 69.364 19.147Z" /> 88 + </svg> 89 + {label} 90 + </span> 91 + ); 92 + }