From 24dd0e22fa5406ec98fe33529c45eb70d218e678 Mon Sep 17 00:00:00 2001 From: rimar1337 <132627503+rimar1337@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:41:27 +0700 Subject: [PATCH] desktop hover profile --- package-lock.json | 2 + package.json | 1 + src/components/UniversalPostRenderer.tsx | 176 ++++++++++++++++------- src/routes/profile.$did/index.tsx | 144 +++++++++++++------ src/utils/followState.ts | 33 +++++ 5 files changed, 256 insertions(+), 100 deletions(-) diff --git a/package-lock.json b/package-lock.json index 053181a..f4d9cbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "@atproto/oauth-client-browser": "^0.3.33", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-slider": "^1.3.6", "@tailwindcss/vite": "^4.0.6", "@tanstack/query-sync-storage-persister": "^5.85.6", @@ -2402,6 +2403,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", diff --git a/package.json b/package.json index e9c1102..fa8d10c 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@atproto/oauth-client-browser": "^0.3.33", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-slider": "^1.3.6", "@tailwindcss/vite": "^4.0.6", "@tanstack/query-sync-storage-persister": "^5.85.6", diff --git a/src/components/UniversalPostRenderer.tsx b/src/components/UniversalPostRenderer.tsx index 5390e2c..6e1b2cc 100644 --- a/src/components/UniversalPostRenderer.tsx +++ b/src/components/UniversalPostRenderer.tsx @@ -2,10 +2,16 @@ import { useNavigate } from "@tanstack/react-router"; import DOMPurify from "dompurify"; import { useAtom } from "jotai"; import { DropdownMenu } from "radix-ui"; +import { HoverCard } from "radix-ui"; import * as React from "react"; import { type SVGProps } from "react"; -import { composerAtom, constellationURLAtom, imgCDNAtom, likedPostsAtom } from "~/utils/atoms"; +import { + composerAtom, + constellationURLAtom, + imgCDNAtom, + likedPostsAtom, +} from "~/utils/atoms"; import { useHydratedEmbed } from "~/utils/useHydrated"; import { useQueryConstellation, @@ -403,7 +409,7 @@ export function UniversalPostRendererATURILoader({ // path: ".reply.parent.uri", // }); - const [constellationurl] = useAtom(constellationURLAtom) + const [constellationurl] = useAtom(constellationURLAtom); const infinitequeryresults = useInfiniteQuery({ ...yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks( @@ -725,24 +731,39 @@ export function UniversalPostRendererRawRecordShim({ error: embedError, } = useHydratedEmbed(postRecord?.value?.embed, resolved?.did); - const [imgcdn] = useAtom(imgCDNAtom) + const [imgcdn] = useAtom(imgCDNAtom); const parsedaturi = new AtUri(aturi); //parseAtUri(aturi); + const fakeprofileviewbasic = React.useMemo( + () => ({ + did: resolved?.did || "", + handle: resolved?.handle || "", + displayName: profileRecord?.value?.displayName || "", + avatar: getAvatarUrl(profileRecord, resolved?.did, imgcdn) || "", + viewer: undefined, + labels: profileRecord?.labels || undefined, + verification: undefined, + }), + [imgcdn, profileRecord, resolved?.did, resolved?.handle] + ); + + const fakeprofileviewdetailed = + React.useMemo( + () => ({ + ...fakeprofileviewbasic, + $type: "app.bsky.actor.defs#profileViewDetailed", + description: profileRecord?.value?.description || undefined, + }), + [fakeprofileviewbasic, profileRecord?.value?.description] + ); + const fakepost = React.useMemo( () => ({ $type: "app.bsky.feed.defs#postView", uri: aturi, cid: postRecord?.cid || "", - author: { - did: resolved?.did || "", - handle: resolved?.handle || "", - displayName: profileRecord?.value?.displayName || "", - avatar: getAvatarUrl(profileRecord, resolved?.did, imgcdn) || "", - viewer: undefined, - labels: profileRecord?.labels || undefined, - verification: undefined, - }, + author: fakeprofileviewbasic, record: postRecord?.value || {}, embed: hydratedEmbed ?? undefined, replyCount: repliesCount ?? 0, @@ -759,14 +780,11 @@ export function UniversalPostRendererRawRecordShim({ postRecord?.cid, postRecord?.value, postRecord?.labels, - resolved?.did, - resolved?.handle, - profileRecord, + fakeprofileviewbasic, hydratedEmbed, repliesCount, repostsCount, likesCount, - imgcdn ] ); @@ -839,6 +857,7 @@ export function UniversalPostRendererRawRecordShim({ } }} post={fakepost} + uprrrsauthor={fakeprofileviewdetailed} salt={aturi} bottomReplyLine={bottomReplyLine} topReplyLine={topReplyLine} @@ -1143,6 +1162,7 @@ export function MdiPlayCircle(props: SVGProps) { //import Masonry from "@mui/lab/Masonry"; import { type $Typed, + AppBskyActorDefs, AppBskyEmbedDefs, AppBskyEmbedExternal, AppBskyEmbedImages, @@ -1172,6 +1192,7 @@ import ReactPlayer from "react-player"; import defaultpfp from "~/../public/favicon.png"; import { useAuth } from "~/providers/UnifiedAuthProvider"; +import { FollowButton, Mutual } from "~/routes/profile.$did"; import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i"; // import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed"; // import type { @@ -1280,6 +1301,7 @@ function randomString(length = 8) { function UniversalPostRenderer({ post, + uprrrsauthor, //setMainItem, //isMainItem, onPostClick, @@ -1304,6 +1326,7 @@ function UniversalPostRenderer({ maxReplies, }: { post: PostView; + uprrrsauthor?: AppBskyActorDefs.ProfileViewDetailed; // optional for now because i havent ported every use to this yet // setMainItem?: React.Dispatch< // React.SetStateAction @@ -1487,43 +1510,90 @@ function UniversalPostRenderer({ className="bg-gray-500 dark:bg-gray-400" /> )} -
- avatar -
+ + +
+ avatar +
+
+ + +
+
+ avatar +
+ +
+
+
+
+
+ {post.author.displayName || post.author.handle}{" "} +
+
+ @{post.author.handle}{" "} +
+
+ {uprrrsauthor?.description && ( +
+ {uprrrsauthor.description} +
+ )} + {/*
+
+
+ 0 +
+
+ Following +
+
+
+
+ 2,900 +
+
+ Followers +
+
+
*/} +
+
+ + {/* */} +
+
+
+
postsData?.pages.flatMap((page) => page.records) ?? [], [postsData] ); - - const [imgcdn] = useAtom(imgCDNAtom) + + const [imgcdn] = useAtom(imgCDNAtom); function getAvatarUrl(p: typeof profile) { const link = p?.avatar?.ref?.["$link"]; @@ -166,43 +160,7 @@ function ProfileComponent() { also delay the backfill to be on demand because it would be pretty intense also save it persistently */} - {identity?.did !== agent?.did ? ( - <> - {!(followRecords?.length && followRecords?.length > 0) ? ( - - ) : ( - - )} - - ) : ( - - )} + @@ -211,7 +169,8 @@ function ProfileComponent() { {/* Info Card */}
{displayName}
-
+
+ {handle}
{description && ( @@ -259,3 +218,94 @@ function ProfileComponent() { ); } + +export function FollowButton({targetdidorhandle}:{targetdidorhandle: string}) { + const {agent} = useAuth() + const {data: identity} = useQueryIdentity(targetdidorhandle); + const queryClient = useQueryClient(); + + const followRecords = useGetFollowState({ + target: identity?.did ?? targetdidorhandle, + user: agent?.did, + }); + + return ( + <> + {identity?.did !== agent?.did ? ( + <> + {!(followRecords?.length && followRecords?.length > 0) ? ( + + ) : ( + + )} + + ) : ( + + )} + + ); +} + + +export function Mutual({targetdidorhandle}:{targetdidorhandle: string}) { + const {agent} = useAuth() + const {data: identity} = useQueryIdentity(targetdidorhandle); + + const mutualfollows = useGetOneToOneState(agent?.did ? { + target: agent?.did, + user: identity?.did ?? targetdidorhandle, + collection: "app.bsky.graph.follow", + path: ".subject" + }:undefined); + + const ismutual: boolean = (!!mutualfollows?.length && mutualfollows.length > 0) + + return ( + <> + {identity?.did !== agent?.did ? ( + <> + {!(ismutual) ? ( + <> + ) : ( +
mutuals
+ )} + + ) : ( + // lmao can someone be mutuals with themselves ?? + <> + )} + + ); +} \ No newline at end of file diff --git a/src/utils/followState.ts b/src/utils/followState.ts index c4c6a23..b6c998c 100644 --- a/src/utils/followState.ts +++ b/src/utils/followState.ts @@ -128,3 +128,36 @@ export function toggleFollow({ }; }); } + + + +export function useGetOneToOneState(params?: { + target: string; + user: string; + collection: string; + path: string; +}): string[] | undefined { + const { data: arbitrarydata } = useQueryConstellation( + params && params.user + ? { + method: "/links", + target: params.target, + // @ts-expect-error overloading sucks so much + collection: params.collection, + path: params.path, + dids: [params.user], + } + : { method: "undefined", target: "whatever" } + // overloading sucks so much + ) as { data: linksRecordsResponse | undefined }; + if (!params || !params.user) return undefined; + const data = arbitrarydata?.linking_records.slice(0, 50) ?? []; + + if (data.length > 0) { + return data.map((linksRecord) => { + return `at://${linksRecord.did}/${linksRecord.collection}/${linksRecord.rkey}`; + }); + } + + return undefined; +} \ No newline at end of file -- 2.51.2