+94
src/components/didlink.tsx
+94
src/components/didlink.tsx
···
1
+
import { A } from "@solidjs/router";
2
+
import { createSignal, createEffect, Show } from "solid-js";
3
+
import { createStore } from "solid-js/store";
4
+
import { Client, CredentialManager, SuccessClientResponse } from "@atcute/client";
5
+
import { Did } from "@atcute/lexicons";
6
+
import { mainSchema } from "@atcute/bluesky/types/app/actor/getProfile";
7
+
8
+
type getProfileResult = SuccessClientResponse<mainSchema, {params: {actor: Did}}>["data"];
9
+
10
+
export const DIDLink = (props: {
11
+
did: Did
12
+
}) => {
13
+
let [hover, setHover] = createSignal(false);
14
+
const [previewHeight, setPreviewHeight] = createSignal(0);
15
+
const [data, setData] = createStore<Record<Did, getProfileResult>>();
16
+
17
+
let rkeyRef!: HTMLSpanElement;
18
+
let previewRef!: HTMLSpanElement;
19
+
20
+
createEffect(async () => {
21
+
if (hover()) {
22
+
setPreviewHeight(previewRef.offsetHeight);
23
+
24
+
if (data[props.did] === undefined) {
25
+
let data = await getData(props.did);
26
+
if (data) setData(props.did, data);
27
+
}
28
+
};
29
+
});
30
+
31
+
const getData = async (did: Did) => {
32
+
const rpc = new Client({
33
+
handler: new CredentialManager({service: "https://public.api.bsky.app"}),
34
+
});
35
+
const res = await rpc.get("app.bsky.actor.getProfile", { params: { actor: did } });
36
+
if (res.ok) {
37
+
return res.data;
38
+
}
39
+
return null;
40
+
}
41
+
42
+
const isOverflowing = (previewHeight: number) =>
43
+
rkeyRef.offsetTop - window.scrollY + previewHeight + 32 > window.innerHeight;
44
+
45
+
return (
46
+
<span
47
+
ref={rkeyRef}
48
+
onmouseover={() => setHover(true)}
49
+
onmouseleave={() => setHover(false)}
50
+
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"
51
+
>
52
+
<A class="text-blue-400 hover:underline active:underline" href={`/at://${props.did}`}>
53
+
{props.did}
54
+
</A>
55
+
<Show when={hover()}>
56
+
<span
57
+
ref={previewRef}
58
+
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"}`}
59
+
>
60
+
<span
61
+
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"
62
+
>
63
+
<Show
64
+
when={data[props.did]?.avatar}
65
+
fallback={<span class="size-9 iconify rounded-full lucide--user-round" />}
66
+
>
67
+
<img
68
+
src={data[props.did].avatar?.replace("img/avatar/", "img/avatar_thumbnail/")}
69
+
class="size-9 iconify rounded-full"
70
+
/>
71
+
</Show>
72
+
73
+
<div class="flex flex-col">
74
+
<Show
75
+
when={data[props.did]?.displayName}
76
+
fallback={<span class="text-sm font-medium">{props.did}</span>}
77
+
>
78
+
<span class="text-sm font-medium">{data[props.did].displayName}</span>
79
+
</Show>
80
+
<Show
81
+
when={data[props.did]?.handle}
82
+
fallback={<span class="text-xs text-neutral-600 dark:text-neutral-400">@handle.invalid</span>}
83
+
>
84
+
<span class="text-xs text-neutral-600 dark:text-neutral-400">
85
+
@{data[props.did].handle}
86
+
</span>
87
+
</Show>
88
+
</div>
89
+
</span>
90
+
</span>
91
+
</Show>
92
+
</span>
93
+
)
94
+
}
+2
-3
src/components/json.tsx
+2
-3
src/components/json.tsx
···
6
6
import { pds } from "./navbar";
7
7
import { addNotification, removeNotification } from "./notification";
8
8
import VideoPlayer from "./video-player";
9
+
import { DIDLink } from "./didlink.tsx";
9
10
10
11
interface AtBlob {
11
12
$type: string;
···
61
62
{part}
62
63
</A>
63
64
: isDid(part) ?
64
-
<A class="text-blue-400 hover:underline active:underline" href={`/at://${part}`}>
65
-
{part}
66
-
</A>
65
+
<DIDLink did={part} />
67
66
: isNsid(part.split("#")[0]) && props.isType ?
68
67
<button
69
68
type="button"