Hovering over a did in a record now show a popup of the related user #17

closed
opened by 5jiji.com targeting main from 5jiji.com/pdsls: did-hover-popup
Changed files
+96 -3
src
components
+94
src/components/didlink.tsx
···
+
import { A } from "@solidjs/router";
+
import { createSignal, createEffect, Show } from "solid-js";
+
import { createStore } from "solid-js/store";
+
import { Client, CredentialManager, SuccessClientResponse } from "@atcute/client";
+
import { Did } from "@atcute/lexicons";
+
import { mainSchema } from "@atcute/bluesky/types/app/actor/getProfile";
+
+
type getProfileResult = SuccessClientResponse<mainSchema, {params: {actor: Did}}>["data"];
+
+
export const DIDLink = (props: {
+
did: Did
+
}) => {
+
let [hover, setHover] = createSignal(false);
+
const [previewHeight, setPreviewHeight] = createSignal(0);
+
const [data, setData] = createStore<Record<Did, getProfileResult>>();
+
+
let rkeyRef!: HTMLSpanElement;
+
let previewRef!: HTMLSpanElement;
+
+
createEffect(async () => {
+
if (hover()) {
+
setPreviewHeight(previewRef.offsetHeight);
+
+
if (data[props.did] === undefined) {
+
let data = await getData(props.did);
+
if (data) setData(props.did, data);
+
}
+
};
+
});
+
+
const getData = async (did: Did) => {
+
const rpc = new Client({
+
handler: new CredentialManager({service: "https://public.api.bsky.app"}),
+
});
+
const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } });
+
if (res.ok) {
+
return res.data;
+
}
+
return null;
+
}
+
+
const isOverflowing = (previewHeight: number) =>
+
rkeyRef.offsetTop - window.scrollY + previewHeight + 32 > window.innerHeight;
+
+
return (
+
<span
+
ref={rkeyRef}
+
onmouseover={() => setHover(true)}
+
onmouseleave={() => setHover(false)}
+
class="relative w-full min-w-0 items-start rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
+
>
+
<A class="text-blue-400 hover:underline active:underline" href={`/at://${props.did}`}>
+
{props.did}
+
</A>
+
<Show when={hover()}>
+
<span
+
ref={previewRef}
+
class={`dark:bg-dark-300 dark:shadow-dark-700 pointer-events-none absolute left-[50%] z-25 block max-h-80 w-max max-w-sm -translate-x-1/2 overflow-hidden rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-xs whitespace-pre-wrap shadow-md sm:max-h-112 lg:max-w-lg dark:border-neutral-700 ${isOverflowing(previewHeight()) ? "bottom-7" : "top-7"}`}
+
>
+
<span
+
class="flex items-center gap-2 rounded-md p-2 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
+
>
+
<Show
+
when={data[props.did]?.avatar}
+
fallback={<span class="size-9 iconify rounded-full lucide--user-round" />}
+
>
+
<img
+
src={data[props.did].avatar?.replace("img/avatar/", "img/avatar_thumbnail/")}
+
class="size-9 iconify rounded-full"
+
/>
+
</Show>
+
+
<div class="flex flex-col">
+
<Show
+
when={data[props.did]?.displayName}
+
fallback={<span class="text-sm font-medium">{props.did}</span>}
+
>
+
<span class="text-sm font-medium">{data[props.did].displayName}</span>
+
</Show>
+
<Show
+
when={data[props.did]?.handle}
+
fallback={<span class="text-xs text-neutral-600 dark:text-neutral-400">@handle.invalid</span>}
+
>
+
<span class="text-xs text-neutral-600 dark:text-neutral-400">
+
@{data[props.did].handle}
+
</span>
+
</Show>
+
</div>
+
</span>
+
</span>
+
</Show>
+
</span>
+
)
+
}
+2 -3
src/components/json.tsx
···
import { pds } from "./navbar";
import { addNotification, removeNotification } from "./notification";
import VideoPlayer from "./video-player";
+
import { DIDLink } from "./didlink.tsx";
interface AtBlob {
$type: string;
···
{part}
</A>
: isDid(part) ?
-
<A class="text-blue-400 hover:underline active:underline" href={`/at://${part}`}>
-
{part}
-
</A>
+
<DIDLink did={part} />
: isNsid(part.split("#")[0]) && props.isType ?
<button
type="button"