mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
wip: new activity sidebar
This commit is contained in:
parent
a8cd34d998
commit
2c8571ecc7
@ -392,8 +392,12 @@ export class Ark {
|
|||||||
const seenIds = new Set<string>();
|
const seenIds = new Set<string>();
|
||||||
const dedupQueue = new Set<string>();
|
const dedupQueue = new Set<string>();
|
||||||
|
|
||||||
|
const relayUrls = [...this.ndk.pool.relays.values()].map(
|
||||||
|
(item) => item.url,
|
||||||
|
);
|
||||||
|
|
||||||
const events = await this.#fetcher.fetchLatestEvents(
|
const events = await this.#fetcher.fetchLatestEvents(
|
||||||
this.#storage.account.relayList,
|
relayUrls,
|
||||||
filter,
|
filter,
|
||||||
limit,
|
limit,
|
||||||
{
|
{
|
||||||
|
@ -28,6 +28,8 @@ export function RepostNote({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@ -72,7 +74,6 @@ export function RepostNote({
|
|||||||
<Note.Repost />
|
<Note.Repost />
|
||||||
<Note.Zap />
|
<Note.Zap />
|
||||||
</div>
|
</div>
|
||||||
N
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Note.Provider>
|
</Note.Provider>
|
||||||
|
@ -41,7 +41,7 @@ export function NoteUser({
|
|||||||
<Avatar.Image
|
<Avatar.Image
|
||||||
src={fallbackAvatar}
|
src={fallbackAvatar}
|
||||||
alt={event.pubkey}
|
alt={event.pubkey}
|
||||||
className="h-6 w-6 rounded-md bg-black dark:bg-white"
|
className="h-6 w-6 shrink-0 object-cover rounded-md bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
<div className="flex flex-1 items-baseline gap-2">
|
<div className="flex flex-1 items-baseline gap-2">
|
||||||
@ -65,7 +65,7 @@ export function NoteUser({
|
|||||||
alt={event.pubkey}
|
alt={event.pubkey}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
className="h-6 w-6 rounded-md"
|
className="h-6 w-6 shrink-0 object-cover rounded-md"
|
||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={300}>
|
||||||
<img
|
<img
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
import { LoaderIcon } from "@lume/icons";
|
import { LoaderIcon } from "@lume/icons";
|
||||||
import { NDKCacheAdapterTauri } from "@lume/ndk-cache-tauri";
|
import { NDKCacheAdapterTauri } from "@lume/ndk-cache-tauri";
|
||||||
import { LumeStorage } from "@lume/storage";
|
import { LumeStorage } from "@lume/storage";
|
||||||
import { QUOTES, delay, sendNativeNotification } from "@lume/utils";
|
import {
|
||||||
|
FETCH_LIMIT,
|
||||||
|
QUOTES,
|
||||||
|
delay,
|
||||||
|
sendNativeNotification,
|
||||||
|
} from "@lume/utils";
|
||||||
import NDK, {
|
import NDK, {
|
||||||
|
NDKEvent,
|
||||||
|
NDKKind,
|
||||||
NDKNip46Signer,
|
NDKNip46Signer,
|
||||||
NDKPrivateKeySigner,
|
NDKPrivateKeySigner,
|
||||||
NDKRelay,
|
NDKRelay,
|
||||||
NDKRelayAuthPolicies,
|
NDKRelayAuthPolicies,
|
||||||
} from "@nostr-dev-kit/ndk";
|
} from "@nostr-dev-kit/ndk";
|
||||||
import { ndkAdapter } from "@nostr-fetch/adapter-ndk";
|
import { ndkAdapter } from "@nostr-fetch/adapter-ndk";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { fetch } from "@tauri-apps/plugin-http";
|
import { fetch } from "@tauri-apps/plugin-http";
|
||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
import { relaunch } from "@tauri-apps/plugin-process";
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
@ -35,6 +43,8 @@ const LumeContext = createContext<Context>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [context, setContext] = useState<Context>(undefined);
|
const [context, setContext] = useState<Context>(undefined);
|
||||||
const [isNewVersion, setIsNewVersion] = useState(false);
|
const [isNewVersion, setIsNewVersion] = useState(false);
|
||||||
|
|
||||||
@ -176,14 +186,41 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
|||||||
const contacts = await user.follows();
|
const contacts = await user.follows();
|
||||||
storage.account.contacts = [...contacts].map((user) => user.pubkey);
|
storage.account.contacts = [...contacts].map((user) => user.pubkey);
|
||||||
|
|
||||||
const relays = await user.relayList();
|
// subscribe for new activity
|
||||||
|
const sub = ndk.subscribe(
|
||||||
|
{
|
||||||
|
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Zap],
|
||||||
|
"#p": [storage.account.pubkey],
|
||||||
|
since: Math.floor(Date.now() / 1000),
|
||||||
|
},
|
||||||
|
{ closeOnEose: false, groupable: false },
|
||||||
|
);
|
||||||
|
|
||||||
if (!relays) storage.account.relayList = ndk.explicitRelayUrls;
|
sub.addListener("event", async (event: NDKEvent) => {
|
||||||
|
const profile = await ark.getUserProfile(event.pubkey);
|
||||||
storage.account.relayList = [
|
switch (event.kind) {
|
||||||
...relays.readRelayUrls,
|
case NDKKind.Text:
|
||||||
...relays.bothRelayUrls,
|
return await sendNativeNotification(
|
||||||
];
|
`${
|
||||||
|
profile.displayName || profile.name || "anon"
|
||||||
|
} has replied to your note`,
|
||||||
|
);
|
||||||
|
case NDKKind.Repost:
|
||||||
|
return await sendNativeNotification(
|
||||||
|
`${
|
||||||
|
profile.displayName || profile.name || "anon"
|
||||||
|
} has reposted to your note`,
|
||||||
|
);
|
||||||
|
case NDKKind.Zap:
|
||||||
|
return await sendNativeNotification(
|
||||||
|
`${
|
||||||
|
profile.displayName || profile.name || "anon"
|
||||||
|
} has zapped to your note`,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// init nostr fetcher
|
// init nostr fetcher
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "@lume/tsconfig/base.json",
|
"extends": "@lume/tsconfig/base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist"
|
"outDir": "dist"
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
@ -106,3 +106,4 @@ export * from "./src/check";
|
|||||||
export * from "./src/popperFilled";
|
export * from "./src/popperFilled";
|
||||||
export * from "./src/composeFilled";
|
export * from "./src/composeFilled";
|
||||||
export * from "./src/settingsFilled";
|
export * from "./src/settingsFilled";
|
||||||
|
export * from "./src/bellFilled";
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
import { SVGProps } from 'react';
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
export function BellIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
export function BellIcon(
|
||||||
return (
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
<svg
|
) {
|
||||||
width={24}
|
return (
|
||||||
height={24}
|
<svg
|
||||||
viewBox="0 0 24 24"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
width="24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
height="24"
|
||||||
{...props}
|
fill="none"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
<path
|
{...props}
|
||||||
d="M16 18.25C15.3267 20.0159 13.7891 21.25 12 21.25C10.2109 21.25 8.67327 20.0159 8 18.25M20.5 18.25L18.9554 8.67345C18.4048 5.2596 15.458 2.75 12 2.75C8.54203 2.75 5.59523 5.2596 5.04461 8.67345L3.5 18.25H20.5Z"
|
>
|
||||||
stroke="currentColor"
|
<path
|
||||||
strokeWidth={1.5}
|
stroke="currentColor"
|
||||||
/>
|
strokeLinecap="round"
|
||||||
</svg>
|
strokeLinejoin="round"
|
||||||
);
|
strokeWidth="2"
|
||||||
|
d="M9.159 17.724a59.522 59.522 0 01-3.733-.297 1.587 1.587 0 01-1.33-2.08c.161-.485.324-.963.367-1.478l.355-4.26a7.207 7.207 0 0114.365 0l.355 4.262c.043.515.206.993.367 1.479a1.587 1.587 0 01-1.33 2.077 59.5 59.5 0 01-3.732.297m-5.684 0c1.893.09 3.79.09 5.684 0m-5.684 0v.434a2.842 2.842 0 105.684 0v-.434"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
23
packages/icons/src/bellFilled.tsx
Normal file
23
packages/icons/src/bellFilled.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
|
export function BellFilledIcon(
|
||||||
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M3.822 9.526a8.207 8.207 0 0116.358 0l.355 4.262c.03.374.15.735.32 1.246a2.587 2.587 0 01-2.17 3.387c-.957.106-1.916.19-2.876.25a3.843 3.843 0 01-7.616 0c-.96-.06-1.92-.143-2.877-.25a2.588 2.588 0 01-2.17-3.39c.17-.51.29-.872.32-1.245l.356-4.26zm6.44 9.24a1.843 1.843 0 003.478 0l-.294.008a60.587 60.587 0 01-3.184-.008z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
@ -1,21 +1,24 @@
|
|||||||
import { SVGProps } from 'react';
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
export function ChatsIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
export function ChatsIcon(
|
||||||
return (
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
<svg
|
) {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
return (
|
||||||
width="24"
|
<svg
|
||||||
height="24"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
width="25"
|
||||||
viewBox="0 0 24 24"
|
height="24"
|
||||||
{...props}
|
fill="none"
|
||||||
>
|
viewBox="0 0 25 24"
|
||||||
<path
|
{...props}
|
||||||
fill="currentColor"
|
>
|
||||||
fillRule="evenodd"
|
<path
|
||||||
d="M19.002 3a3 3 0 013 3v6a3 3 0 01-3 3h-1v1a3 3 0 01-3 3h-4.24l-4.274 2.374a1 1 0 01-1.486-.874V19a3 3 0 01-3-3v-6a3 3 0 013-3h1V6a3 3 0 013-3h10zm-11 4h7a3 3 0 013 3v3h1a1 1 0 001-1V6a1 1 0 00-1-1h-10a1 1 0 00-1 1v1z"
|
stroke="currentColor"
|
||||||
clipRule="evenodd"
|
strokeLinecap="round"
|
||||||
></path>
|
strokeLinejoin="round"
|
||||||
</svg>
|
strokeWidth="2"
|
||||||
);
|
d="M17.55 18.718l1.74.124c.762.055 1.143.082 1.428-.053a1.2 1.2 0 00.57-.57c.136-.286.109-.667.054-1.429l-.124-1.74c-.02-.28-.03-.42-.032-.562-.004-.258-.007-.122.009-.38.009-.142.088-.811.248-2.15A8 8 0 006.884 6.5m7.416 9.1a5.4 5.4 0 10-10.733.856c.096.6.144.9.152.992.012.139.011.107.01.247 0 .093-.008.204-.023.427l-.1 1.386c-.036.515-.055.772.037.964a.81.81 0 00.385.385c.192.091.45.073.964.036l1.385-.099c.224-.015.335-.024.428-.024.14 0 .108-.001.247.011.093.008.393.056.992.152a5.387 5.387 0 005.06-1.942A5.377 5.377 0 0014.3 15.6z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
33
packages/ui/src/activity/column.tsx
Normal file
33
packages/ui/src/activity/column.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { activityAtom } from "@lume/utils";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { useAtomValue } from "jotai";
|
||||||
|
import { ActivityContent } from "./content";
|
||||||
|
|
||||||
|
export function Activity() {
|
||||||
|
const isActivityOpen = useAtomValue(activityAtom);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence initial={false} mode="wait">
|
||||||
|
{isActivityOpen ? (
|
||||||
|
<motion.div
|
||||||
|
key={isActivityOpen ? "activity-open" : "activity-close"}
|
||||||
|
layout
|
||||||
|
initial={{ scale: 0.9, opacity: 0, translateX: -20 }}
|
||||||
|
animate={{
|
||||||
|
scale: [0.95, 1],
|
||||||
|
opacity: [0.5, 1],
|
||||||
|
translateX: [-10, 0],
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
scale: [0.95, 0.9],
|
||||||
|
opacity: [0.5, 0],
|
||||||
|
translateX: [-10, -20],
|
||||||
|
}}
|
||||||
|
className="h-full w-[350px] px-1 pb-1 shrink-0"
|
||||||
|
>
|
||||||
|
<ActivityContent />
|
||||||
|
</motion.div>
|
||||||
|
) : null}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
101
packages/ui/src/activity/content.tsx
Normal file
101
packages/ui/src/activity/content.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { useArk, useStorage } from "@lume/ark";
|
||||||
|
import { LoaderIcon } from "@lume/icons";
|
||||||
|
import { FETCH_LIMIT } from "@lume/utils";
|
||||||
|
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { VList } from "virtua";
|
||||||
|
import { ReplyActivity } from "./reply";
|
||||||
|
import { RepostActivity } from "./repost";
|
||||||
|
import { ZapActivity } from "./zap";
|
||||||
|
|
||||||
|
export function ActivityContent() {
|
||||||
|
const ark = useArk();
|
||||||
|
const storage = useStorage();
|
||||||
|
|
||||||
|
const { isLoading, data, hasNextPage, isFetchingNextPage, fetchNextPage } =
|
||||||
|
useInfiniteQuery({
|
||||||
|
queryKey: ["activity"],
|
||||||
|
initialPageParam: 0,
|
||||||
|
queryFn: async ({
|
||||||
|
signal,
|
||||||
|
pageParam,
|
||||||
|
}: {
|
||||||
|
signal: AbortSignal;
|
||||||
|
pageParam: number;
|
||||||
|
}) => {
|
||||||
|
const events = await ark.getInfiniteEvents({
|
||||||
|
filter: {
|
||||||
|
kinds: [NDKKind.Zap],
|
||||||
|
"#p": [storage.account.pubkey],
|
||||||
|
},
|
||||||
|
limit: 100,
|
||||||
|
pageParam,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
return events;
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
const lastEvent = lastPage.at(-1);
|
||||||
|
if (!lastEvent) return;
|
||||||
|
return lastEvent.created_at - 1;
|
||||||
|
},
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const allEvents = useMemo(
|
||||||
|
() => (data ? data.pages.flatMap((page) => page) : []),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderEvent = useCallback((event: NDKEvent) => {
|
||||||
|
if (event.pubkey === storage.account.pubkey) return null;
|
||||||
|
|
||||||
|
switch (event.kind) {
|
||||||
|
case NDKKind.Text:
|
||||||
|
return <ReplyActivity key={event.id} event={event} />;
|
||||||
|
case NDKKind.Repost:
|
||||||
|
return <RepostActivity key={event.id} event={event} />;
|
||||||
|
case NDKKind.Zap:
|
||||||
|
return <ZapActivity key={event.id} event={event} />;
|
||||||
|
default:
|
||||||
|
return <ReplyActivity key={event.id} event={event} />;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col justify-between rounded-xl overflow-hidden bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/10">
|
||||||
|
<div className="h-full w-full min-h-0">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="w-[350px] h-full flex items-center justify-center">
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : allEvents.length < 1 ? (
|
||||||
|
<div className="w-full h-full flex flex-col items-center justify-center">
|
||||||
|
<p className="mb-2 text-2xl">🎉</p>
|
||||||
|
<p className="text-center font-medium">Yo! Nothing new yet.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
renderEvent(allEvents[0])
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="h-16 shrink-0 px-3 flex items-center gap-3 bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-11 flex-1 inline-flex items-center justify-center rounded-xl font-medium bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="h-11 flex-1 inline-flex items-center justify-center rounded-xl font-medium bg-blue-500 text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
48
packages/ui/src/activity/reply.tsx
Normal file
48
packages/ui/src/activity/reply.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Note, useArk } from "@lume/ark";
|
||||||
|
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||||
|
import { ActivityRootNote } from "./rootNote";
|
||||||
|
|
||||||
|
export function ReplyActivity({ event }: { event: NDKEvent }) {
|
||||||
|
const ark = useArk();
|
||||||
|
const thread = ark.getEventThread({ tags: event.tags });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full pb-3 flex flex-col justify-between">
|
||||||
|
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
|
||||||
|
<h3 className="text-center font-semibold leading-tight">
|
||||||
|
Conversation
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-blue-500 font-medium leading-tight">
|
||||||
|
@ Someone has replied to your note
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="px-3">
|
||||||
|
{thread ? (
|
||||||
|
<div className="flex flex-col gap-3 mb-1">
|
||||||
|
<ActivityRootNote eventId={thread.rootEventId} />
|
||||||
|
<ActivityRootNote eventId={thread.replyEventId} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="mt-3 flex flex-col gap-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<p className="text-teal-500 font-medium">New reply</p>
|
||||||
|
<div className="flex-1 h-px bg-teal-300" />
|
||||||
|
<div className="w-4 shrink-0 h-px bg-teal-300" />
|
||||||
|
</div>
|
||||||
|
<Note.Provider event={event}>
|
||||||
|
<Note.Root>
|
||||||
|
<div className="flex items-center justify-between px-3 h-14">
|
||||||
|
<Note.User className="flex-1 pr-1" />
|
||||||
|
</div>
|
||||||
|
<Note.Content className="min-w-0 px-3" />
|
||||||
|
<div className="flex items-center justify-between px-3 h-14">
|
||||||
|
<Note.Pin />
|
||||||
|
<div className="inline-flex items-center gap-10" />
|
||||||
|
</div>
|
||||||
|
</Note.Root>
|
||||||
|
</Note.Provider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
33
packages/ui/src/activity/repost.tsx
Normal file
33
packages/ui/src/activity/repost.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { formatCreatedAt } from "@lume/utils";
|
||||||
|
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||||
|
import { User } from "../user";
|
||||||
|
import { ActivityRootNote } from "./rootNote";
|
||||||
|
|
||||||
|
export function RepostActivity({ event }: { event: NDKEvent }) {
|
||||||
|
const repostId = event.tags.find((el) => el[0] === "e")[1];
|
||||||
|
const createdAt = formatCreatedAt(event.created_at);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full pb-3 flex flex-col justify-between">
|
||||||
|
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
|
||||||
|
<h3 className="text-center font-semibold leading-tight">Boost</h3>
|
||||||
|
<p className="text-sm text-blue-500 font-medium leading-tight">
|
||||||
|
@ Someone has reposted to your note
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="px-3">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<User pubkey={event.pubkey} variant="notify2" />
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<p className="text-teal-500 font-medium">Reposted</p>
|
||||||
|
<div className="flex-1 h-px bg-teal-300" />
|
||||||
|
<div className="w-4 shrink-0 h-px bg-teal-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex flex-col gap-3">
|
||||||
|
<ActivityRootNote eventId={repostId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
40
packages/ui/src/activity/rootNote.tsx
Normal file
40
packages/ui/src/activity/rootNote.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Note, useEvent } from "@lume/ark";
|
||||||
|
|
||||||
|
export function ActivityRootNote({ eventId }: { eventId: string }) {
|
||||||
|
const { isLoading, isError, data } = useEvent(eventId);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="relative flex gap-3">
|
||||||
|
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||||
|
<div className="h-4 w-full animate-pulse bg-neutral-300 dark:bg-neutral-700" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="relative flex gap-3">
|
||||||
|
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
|
||||||
|
Failed to fetch event
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Note.Provider event={data}>
|
||||||
|
<Note.Root>
|
||||||
|
<div className="flex items-center justify-between px-3 h-14">
|
||||||
|
<Note.User className="flex-1 pr-1" />
|
||||||
|
</div>
|
||||||
|
<Note.Content className="min-w-0 px-3" />
|
||||||
|
<div className="flex items-center justify-between px-3 h-14">
|
||||||
|
<Note.Pin />
|
||||||
|
<div className="inline-flex items-center gap-10" />
|
||||||
|
</div>
|
||||||
|
</Note.Root>
|
||||||
|
</Note.Provider>
|
||||||
|
);
|
||||||
|
}
|
37
packages/ui/src/activity/zap.tsx
Normal file
37
packages/ui/src/activity/zap.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { compactNumber, formatCreatedAt } from "@lume/utils";
|
||||||
|
import { NDKEvent, zapInvoiceFromEvent } from "@nostr-dev-kit/ndk";
|
||||||
|
import { User } from "../user";
|
||||||
|
import { ActivityRootNote } from "./rootNote";
|
||||||
|
|
||||||
|
export function ZapActivity({ event }: { event: NDKEvent }) {
|
||||||
|
const zapEventId = event.tags.find((tag) => tag[0] === "e")[1];
|
||||||
|
const invoice = zapInvoiceFromEvent(event);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full pb-3 flex flex-col justify-between">
|
||||||
|
<div className="h-14 border-b border-neutral-100 dark:border-neutral-900 flex flex-col items-center justify-center px-3">
|
||||||
|
<h3 className="text-center font-semibold leading-tight">Zap</h3>
|
||||||
|
<p className="text-sm text-blue-500 font-medium leading-tight">
|
||||||
|
@ Someone love your note
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="px-3">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<User
|
||||||
|
pubkey={event.pubkey}
|
||||||
|
variant="notify2"
|
||||||
|
subtext={`${compactNumber.format(invoice.amount)} sats`}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<p className="text-teal-500 font-medium">Zapped</p>
|
||||||
|
<div className="flex-1 h-px bg-teal-300" />
|
||||||
|
<div className="w-4 shrink-0 h-px bg-teal-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex flex-col gap-3">
|
||||||
|
<ActivityRootNote eventId={zapEventId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -288,7 +288,7 @@ export function EditorForm() {
|
|||||||
}, [filters.length, editor, index, search, target]);
|
}, [filters.length, editor, index, search, target]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col justify-between rounded-xl overflow-hidden bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
|
<div className="w-full h-full flex flex-col justify-between rounded-xl overflow-hidden bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/10">
|
||||||
<Slate
|
<Slate
|
||||||
editor={editor}
|
editor={editor}
|
||||||
initialValue={editorValue}
|
initialValue={editorValue}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { type Platform } from "@tauri-apps/plugin-os";
|
import { type Platform } from "@tauri-apps/plugin-os";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { Activity } from "../activity/column";
|
||||||
import { Editor } from "../editor/column";
|
import { Editor } from "../editor/column";
|
||||||
import { Navigation } from "../navigation";
|
import { Navigation } from "../navigation";
|
||||||
import { WindowTitleBar } from "../titlebar";
|
import { WindowTitleBar } from "../titlebar";
|
||||||
@ -21,6 +22,7 @@ export function AppLayout({ platform }: { platform: Platform }) {
|
|||||||
<div className="flex w-full h-full min-h-0">
|
<div className="flex w-full h-full min-h-0">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<Editor />
|
<Editor />
|
||||||
|
<Activity />
|
||||||
<div className="flex-1 h-full px-1 pb-1">
|
<div className="flex-1 h-full px-1 pb-1">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,7 +5,7 @@ export function HomeLayout() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OnboardingModal />
|
<OnboardingModal />
|
||||||
<div className="h-full w-full rounded-xl overflow-hidden bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-[inset_0_0_0.5px_1px_hsla(0,0%,100%,0.075),0_0_0_1px_hsla(0,0%,0%,0.05),0_0.3px_0.4px_hsla(0,0%,0%,0.02),0_0.9px_1.5px_hsla(0,0%,0%,0.045),0_3.5px_6px_hsla(0,0%,0%,0.09)]">
|
<div className="h-full w-full rounded-xl overflow-hidden bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/10">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
|
BellFilledIcon,
|
||||||
|
BellIcon,
|
||||||
ComposeFilledIcon,
|
ComposeFilledIcon,
|
||||||
ComposeIcon,
|
ComposeIcon,
|
||||||
DepotFilledIcon,
|
DepotFilledIcon,
|
||||||
DepotIcon,
|
DepotIcon,
|
||||||
HomeFilledIcon,
|
HomeFilledIcon,
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
NwcFilledIcon,
|
|
||||||
NwcIcon,
|
NwcIcon,
|
||||||
RelayFilledIcon,
|
RelayFilledIcon,
|
||||||
RelayIcon,
|
RelayIcon,
|
||||||
SettingsFilledIcon,
|
SettingsFilledIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
} from "@lume/icons";
|
} from "@lume/icons";
|
||||||
import { cn, editorAtom } from "@lume/utils";
|
import { activityAtom, cn, editorAtom } from "@lume/utils";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
import { NavLink } from "react-router-dom";
|
import { NavLink } from "react-router-dom";
|
||||||
@ -20,6 +21,8 @@ import { ActiveAccount } from "./account/active";
|
|||||||
|
|
||||||
export function Navigation() {
|
export function Navigation() {
|
||||||
const [isEditorOpen, setIsEditorOpen] = useAtom(editorAtom);
|
const [isEditorOpen, setIsEditorOpen] = useAtom(editorAtom);
|
||||||
|
const [isActvityOpen, setIsActvityOpen] = useAtom(activityAtom);
|
||||||
|
|
||||||
useHotkeys("meta+n", () => setIsEditorOpen((state) => !state), []);
|
useHotkeys("meta+n", () => setIsEditorOpen((state) => !state), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -29,7 +32,10 @@ export function Navigation() {
|
|||||||
<ActiveAccount />
|
<ActiveAccount />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsEditorOpen((prev) => !prev)}
|
onClick={() => {
|
||||||
|
setIsEditorOpen((state) => !state);
|
||||||
|
setIsActvityOpen(false);
|
||||||
|
}}
|
||||||
className="flex items-center justify-center h-auto w-full text-black aspect-square rounded-xl bg-black/5 hover:bg-blue-500 hover:text-white dark:bg-white/5 dark:text-white dark:hover:bg-blue-500"
|
className="flex items-center justify-center h-auto w-full text-black aspect-square rounded-xl bg-black/5 hover:bg-blue-500 hover:text-white dark:bg-white/5 dark:text-white dark:hover:bg-blue-500"
|
||||||
>
|
>
|
||||||
{isEditorOpen ? (
|
{isEditorOpen ? (
|
||||||
@ -63,50 +69,30 @@ export function Navigation() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink
|
<button
|
||||||
to="/relays"
|
type="button"
|
||||||
preventScrollReset={true}
|
onClick={() => {
|
||||||
|
setIsActvityOpen((state) => !state);
|
||||||
|
setIsEditorOpen(false);
|
||||||
|
}}
|
||||||
className="inline-flex flex-col items-center justify-center"
|
className="inline-flex flex-col items-center justify-center"
|
||||||
>
|
>
|
||||||
{({ isActive }) => (
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
|
||||||
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
|
isActvityOpen
|
||||||
isActive
|
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
|
||||||
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
|
: "text-black/50 dark:text-neutral-400",
|
||||||
: "text-black/50 dark:text-neutral-400",
|
)}
|
||||||
)}
|
>
|
||||||
>
|
{isActvityOpen ? (
|
||||||
{isActive ? (
|
<BellFilledIcon className="size-6" />
|
||||||
<RelayFilledIcon className="size-6" />
|
) : (
|
||||||
) : (
|
<BellIcon className="size-6" />
|
||||||
<RelayIcon className="size-6" />
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</button>
|
||||||
)}
|
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
|
||||||
to="/depot"
|
|
||||||
preventScrollReset={true}
|
|
||||||
className="inline-flex flex-col items-center justify-center"
|
|
||||||
>
|
|
||||||
{({ isActive }) => (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
|
|
||||||
isActive
|
|
||||||
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
|
|
||||||
: "text-black/50 dark:text-neutral-400",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isActive ? (
|
|
||||||
<DepotFilledIcon className="text-black size-6 dark:text-white" />
|
|
||||||
) : (
|
|
||||||
<DepotIcon className="size-6" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/nwc"
|
to="/nwc"
|
||||||
preventScrollReset={true}
|
preventScrollReset={true}
|
||||||
|
@ -2,11 +2,8 @@ import { useProfile } from "@lume/ark";
|
|||||||
import { RepostIcon } from "@lume/icons";
|
import { RepostIcon } from "@lume/icons";
|
||||||
import { displayNpub, formatCreatedAt } from "@lume/utils";
|
import { displayNpub, formatCreatedAt } from "@lume/utils";
|
||||||
import * as Avatar from "@radix-ui/react-avatar";
|
import * as Avatar from "@radix-ui/react-avatar";
|
||||||
import * as HoverCard from "@radix-ui/react-hover-card";
|
|
||||||
import { minidenticon } from "minidenticons";
|
import { minidenticon } from "minidenticons";
|
||||||
import { memo, useMemo } from "react";
|
import { memo, useMemo } from "react";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { NIP05 } from "./nip05";
|
|
||||||
|
|
||||||
export const User = memo(function User({
|
export const User = memo(function User({
|
||||||
pubkey,
|
pubkey,
|
||||||
@ -21,6 +18,7 @@ export const User = memo(function User({
|
|||||||
| "simple"
|
| "simple"
|
||||||
| "mention"
|
| "mention"
|
||||||
| "notify"
|
| "notify"
|
||||||
|
| "notify2"
|
||||||
| "repost"
|
| "repost"
|
||||||
| "chat"
|
| "chat"
|
||||||
| "large"
|
| "large"
|
||||||
@ -105,6 +103,55 @@ export const User = memo(function User({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (variant === "notify2") {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar.Root className="h-8 w-8 shrink-0">
|
||||||
|
<Avatar.Image
|
||||||
|
src={fallbackAvatar}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||||
|
/>
|
||||||
|
</Avatar.Root>
|
||||||
|
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
|
{fallbackName}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar.Root className="h-8 w-8 shrink-0">
|
||||||
|
<Avatar.Image
|
||||||
|
src={user?.picture || user?.image}
|
||||||
|
alt={pubkey}
|
||||||
|
loading="eager"
|
||||||
|
decoding="async"
|
||||||
|
className="h-8 w-8 rounded-md"
|
||||||
|
/>
|
||||||
|
<Avatar.Fallback delayMs={300}>
|
||||||
|
<img
|
||||||
|
src={fallbackAvatar}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||||
|
/>
|
||||||
|
</Avatar.Fallback>
|
||||||
|
</Avatar.Root>
|
||||||
|
<div className="inline-flex items-center gap-1">
|
||||||
|
<h5 className="max-w-[8rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
|
{user?.name ||
|
||||||
|
user?.display_name ||
|
||||||
|
user?.displayName ||
|
||||||
|
fallbackName}
|
||||||
|
</h5>
|
||||||
|
<p>{subtext}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (variant === "notify") {
|
if (variant === "notify") {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@ -129,7 +176,7 @@ export const User = memo(function User({
|
|||||||
<Avatar.Image
|
<Avatar.Image
|
||||||
src={user?.picture || user?.image}
|
src={user?.picture || user?.image}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
loading="lazy"
|
loading="eager"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
className="h-8 w-8 rounded-md"
|
className="h-8 w-8 rounded-md"
|
||||||
/>
|
/>
|
||||||
@ -525,105 +572,36 @@ export const User = memo(function User({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HoverCard.Root>
|
<div className="flex items-center gap-3 px-3">
|
||||||
<div className="flex items-center gap-3 px-3">
|
<Avatar.Root className="h-9 w-9 shrink-0">
|
||||||
<HoverCard.Trigger asChild>
|
<Avatar.Image
|
||||||
<Avatar.Root className="h-9 w-9 shrink-0">
|
src={user?.picture || user?.image}
|
||||||
<Avatar.Image
|
alt={pubkey}
|
||||||
src={user?.picture || user?.image}
|
loading="lazy"
|
||||||
alt={pubkey}
|
decoding="async"
|
||||||
loading="lazy"
|
className="h-9 w-9 rounded-lg bg-white object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
|
||||||
decoding="async"
|
/>
|
||||||
className="h-9 w-9 rounded-lg bg-white object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50"
|
<Avatar.Fallback delayMs={300}>
|
||||||
/>
|
<img
|
||||||
<Avatar.Fallback delayMs={300}>
|
src={fallbackAvatar}
|
||||||
<img
|
alt={pubkey}
|
||||||
src={fallbackAvatar}
|
className="h-9 w-9 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
||||||
alt={pubkey}
|
/>
|
||||||
className="h-9 w-9 rounded-lg bg-black ring-1 ring-neutral-200/50 dark:bg-white dark:ring-neutral-800/50"
|
</Avatar.Fallback>
|
||||||
/>
|
</Avatar.Root>
|
||||||
</Avatar.Fallback>
|
<div className="flex h-6 flex-1 items-start gap-2">
|
||||||
</Avatar.Root>
|
<div className="max-w-[15rem] truncate font-semibold text-neutral-950 dark:text-neutral-50">
|
||||||
</HoverCard.Trigger>
|
{user?.name ||
|
||||||
<div className="flex h-6 flex-1 items-start gap-2">
|
user?.display_name ||
|
||||||
<div className="max-w-[15rem] truncate font-semibold text-neutral-950 dark:text-neutral-50">
|
user?.displayName ||
|
||||||
{user?.name ||
|
fallbackName}
|
||||||
user?.display_name ||
|
</div>
|
||||||
user?.displayName ||
|
<div className="ml-auto inline-flex items-center gap-3">
|
||||||
fallbackName}
|
<div className="text-neutral-500 dark:text-neutral-400">
|
||||||
</div>
|
{createdAt}
|
||||||
<div className="ml-auto inline-flex items-center gap-3">
|
|
||||||
<div className="text-neutral-500 dark:text-neutral-400">
|
|
||||||
{createdAt}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoverCard.Portal>
|
</div>
|
||||||
<HoverCard.Content
|
|
||||||
className="ml-4 w-[300px] overflow-hidden rounded-xl border border-neutral-200 bg-neutral-100 shadow-lg data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=top]:animate-slideDownAndFade data-[state=open]:transition-all focus:outline-none dark:border-neutral-800 dark:bg-neutral-900"
|
|
||||||
sideOffset={5}
|
|
||||||
>
|
|
||||||
<div className="flex gap-2.5 border-b border-neutral-200 px-3 py-3 dark:border-neutral-800">
|
|
||||||
<Avatar.Root className="shrink-0">
|
|
||||||
<Avatar.Image
|
|
||||||
src={user?.picture || user?.image}
|
|
||||||
alt={pubkey}
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
className="h-10 w-10 rounded-lg object-cover"
|
|
||||||
/>
|
|
||||||
<Avatar.Fallback delayMs={300}>
|
|
||||||
<img
|
|
||||||
src={fallbackAvatar}
|
|
||||||
alt={pubkey}
|
|
||||||
className="h-10 w-10 rounded-lg bg-black dark:bg-white"
|
|
||||||
/>
|
|
||||||
</Avatar.Fallback>
|
|
||||||
</Avatar.Root>
|
|
||||||
<div className="flex flex-1 flex-col gap-2">
|
|
||||||
<div className="inline-flex flex-col">
|
|
||||||
<h5 className="text-sm font-semibold">
|
|
||||||
{user?.name ||
|
|
||||||
user?.display_name ||
|
|
||||||
user?.displayName ||
|
|
||||||
user?.username}
|
|
||||||
</h5>
|
|
||||||
{user?.nip05 ? (
|
|
||||||
<NIP05
|
|
||||||
pubkey={pubkey}
|
|
||||||
nip05={user.nip05}
|
|
||||||
className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-300"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="max-w-[15rem] truncate text-sm text-neutral-500 dark:text-neutral-300">
|
|
||||||
{fallbackName}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="line-clamp-3 break-all text-sm leading-tight text-neutral-900 dark:text-neutral-100">
|
|
||||||
{user?.about}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 px-3 py-3">
|
|
||||||
<Link
|
|
||||||
to={`/users/${pubkey}`}
|
|
||||||
className="inline-flex h-9 flex-1 items-center justify-center rounded-lg bg-neutral-200 text-sm font-semibold hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
View profile
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={`/chats/${pubkey}`}
|
|
||||||
className="inline-flex h-9 flex-1 items-center justify-center rounded-lg bg-neutral-200 text-sm font-semibold hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
Message
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</HoverCard.Content>
|
|
||||||
</HoverCard.Portal>
|
|
||||||
</HoverCard.Root>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -9,3 +9,6 @@ export const editorValueAtom = atom([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const onboardingAtom = atom(false);
|
export const onboardingAtom = atom(false);
|
||||||
|
|
||||||
|
export const activityAtom = atom(false);
|
||||||
|
export const activityUnreadAtom = atom(0);
|
||||||
|
Loading…
Reference in New Issue
Block a user