wip: new activity sidebar

This commit is contained in:
reya 2024-01-11 21:00:42 +07:00
parent a8cd34d998
commit 2c8571ecc7
21 changed files with 537 additions and 203 deletions

View File

@ -392,8 +392,12 @@ export class Ark {
const seenIds = 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(
this.#storage.account.relayList,
relayUrls,
filter,
limit,
{

View File

@ -28,6 +28,8 @@ export function RepostNote({
}
},
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
});
if (isLoading) {
@ -72,7 +74,6 @@ export function RepostNote({
<Note.Repost />
<Note.Zap />
</div>
N
</div>
</div>
</Note.Provider>

View File

@ -41,7 +41,7 @@ export function NoteUser({
<Avatar.Image
src={fallbackAvatar}
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>
<div className="flex flex-1 items-baseline gap-2">
@ -65,7 +65,7 @@ export function NoteUser({
alt={event.pubkey}
loading="eager"
decoding="async"
className="h-6 w-6 rounded-md"
className="h-6 w-6 shrink-0 object-cover rounded-md"
/>
<Avatar.Fallback delayMs={300}>
<img

View File

@ -1,14 +1,22 @@
import { LoaderIcon } from "@lume/icons";
import { NDKCacheAdapterTauri } from "@lume/ndk-cache-tauri";
import { LumeStorage } from "@lume/storage";
import { QUOTES, delay, sendNativeNotification } from "@lume/utils";
import {
FETCH_LIMIT,
QUOTES,
delay,
sendNativeNotification,
} from "@lume/utils";
import NDK, {
NDKEvent,
NDKKind,
NDKNip46Signer,
NDKPrivateKeySigner,
NDKRelay,
NDKRelayAuthPolicies,
} from "@nostr-dev-kit/ndk";
import { ndkAdapter } from "@nostr-fetch/adapter-ndk";
import { useQueryClient } from "@tanstack/react-query";
import { fetch } from "@tauri-apps/plugin-http";
import { platform } from "@tauri-apps/plugin-os";
import { relaunch } from "@tauri-apps/plugin-process";
@ -35,6 +43,8 @@ const LumeContext = createContext<Context>({
});
const LumeProvider = ({ children }: PropsWithChildren<object>) => {
const queryClient = useQueryClient();
const [context, setContext] = useState<Context>(undefined);
const [isNewVersion, setIsNewVersion] = useState(false);
@ -176,14 +186,41 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
const contacts = await user.follows();
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;
storage.account.relayList = [
...relays.readRelayUrls,
...relays.bothRelayUrls,
];
sub.addListener("event", async (event: NDKEvent) => {
const profile = await ark.getUserProfile(event.pubkey);
switch (event.kind) {
case NDKKind.Text:
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

View File

@ -1,8 +1,8 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -106,3 +106,4 @@ export * from "./src/check";
export * from "./src/popperFilled";
export * from "./src/composeFilled";
export * from "./src/settingsFilled";
export * from "./src/bellFilled";

View File

@ -1,20 +1,24 @@
import { SVGProps } from 'react';
import { SVGProps } from "react";
export function BellIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
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"
strokeWidth={1.5}
/>
</svg>
);
export function BellIcon(
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
stroke="currentColor"
strokeLinecap="round"
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>
);
}

View 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>
);
}

View File

@ -1,21 +1,24 @@
import { SVGProps } from 'react';
import { SVGProps } from "react";
export function ChatsIcon(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="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"
clipRule="evenodd"
></path>
</svg>
);
export function ChatsIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="25"
height="24"
fill="none"
viewBox="0 0 25 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -288,7 +288,7 @@ export function EditorForm() {
}, [filters.length, editor, index, search, target]);
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
editor={editor}
initialValue={editorValue}

View File

@ -1,6 +1,7 @@
import { type Platform } from "@tauri-apps/plugin-os";
import { Outlet } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { Activity } from "../activity/column";
import { Editor } from "../editor/column";
import { Navigation } from "../navigation";
import { WindowTitleBar } from "../titlebar";
@ -21,6 +22,7 @@ export function AppLayout({ platform }: { platform: Platform }) {
<div className="flex w-full h-full min-h-0">
<Navigation />
<Editor />
<Activity />
<div className="flex-1 h-full px-1 pb-1">
<Outlet />
</div>

View File

@ -5,7 +5,7 @@ export function HomeLayout() {
return (
<>
<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 />
</div>
</>

View File

@ -1,18 +1,19 @@
import {
BellFilledIcon,
BellIcon,
ComposeFilledIcon,
ComposeIcon,
DepotFilledIcon,
DepotIcon,
HomeFilledIcon,
HomeIcon,
NwcFilledIcon,
NwcIcon,
RelayFilledIcon,
RelayIcon,
SettingsFilledIcon,
SettingsIcon,
} from "@lume/icons";
import { cn, editorAtom } from "@lume/utils";
import { activityAtom, cn, editorAtom } from "@lume/utils";
import { useAtom } from "jotai";
import { useHotkeys } from "react-hotkeys-hook";
import { NavLink } from "react-router-dom";
@ -20,6 +21,8 @@ import { ActiveAccount } from "./account/active";
export function Navigation() {
const [isEditorOpen, setIsEditorOpen] = useAtom(editorAtom);
const [isActvityOpen, setIsActvityOpen] = useAtom(activityAtom);
useHotkeys("meta+n", () => setIsEditorOpen((state) => !state), []);
return (
@ -29,7 +32,10 @@ export function Navigation() {
<ActiveAccount />
<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"
>
{isEditorOpen ? (
@ -63,50 +69,30 @@ export function Navigation() {
</div>
)}
</NavLink>
<NavLink
to="/relays"
preventScrollReset={true}
<button
type="button"
onClick={() => {
setIsActvityOpen((state) => !state);
setIsEditorOpen(false);
}}
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 ? (
<RelayFilledIcon className="size-6" />
) : (
<RelayIcon className="size-6" />
)}
</div>
)}
</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>
<div
className={cn(
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
isActvityOpen
? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
: "text-black/50 dark:text-neutral-400",
)}
>
{isActvityOpen ? (
<BellFilledIcon className="size-6" />
) : (
<BellIcon className="size-6" />
)}
</div>
</button>
<NavLink
to="/nwc"
preventScrollReset={true}

View File

@ -2,11 +2,8 @@ import { useProfile } from "@lume/ark";
import { RepostIcon } from "@lume/icons";
import { displayNpub, formatCreatedAt } from "@lume/utils";
import * as Avatar from "@radix-ui/react-avatar";
import * as HoverCard from "@radix-ui/react-hover-card";
import { minidenticon } from "minidenticons";
import { memo, useMemo } from "react";
import { Link } from "react-router-dom";
import { NIP05 } from "./nip05";
export const User = memo(function User({
pubkey,
@ -21,6 +18,7 @@ export const User = memo(function User({
| "simple"
| "mention"
| "notify"
| "notify2"
| "repost"
| "chat"
| "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 (isLoading) {
return (
@ -129,7 +176,7 @@ export const User = memo(function User({
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
loading="eager"
decoding="async"
className="h-8 w-8 rounded-md"
/>
@ -525,105 +572,36 @@ export const User = memo(function User({
}
return (
<HoverCard.Root>
<div className="flex items-center gap-3 px-3">
<HoverCard.Trigger asChild>
<Avatar.Root className="h-9 w-9 shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
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
src={fallbackAvatar}
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>
</HoverCard.Trigger>
<div className="flex h-6 flex-1 items-start gap-2">
<div className="max-w-[15rem] truncate font-semibold text-neutral-950 dark:text-neutral-50">
{user?.name ||
user?.display_name ||
user?.displayName ||
fallbackName}
</div>
<div className="ml-auto inline-flex items-center gap-3">
<div className="text-neutral-500 dark:text-neutral-400">
{createdAt}
</div>
<div className="flex items-center gap-3 px-3">
<Avatar.Root className="h-9 w-9 shrink-0">
<Avatar.Image
src={user?.picture || user?.image}
alt={pubkey}
loading="lazy"
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
src={fallbackAvatar}
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>
<div className="flex h-6 flex-1 items-start gap-2">
<div className="max-w-[15rem] truncate font-semibold text-neutral-950 dark:text-neutral-50">
{user?.name ||
user?.display_name ||
user?.displayName ||
fallbackName}
</div>
<div className="ml-auto inline-flex items-center gap-3">
<div className="text-neutral-500 dark:text-neutral-400">
{createdAt}
</div>
</div>
</div>
<HoverCard.Portal>
<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>
</div>
);
});

View File

@ -9,3 +9,6 @@ export const editorValueAtom = atom([
]);
export const onboardingAtom = atom(false);
export const activityAtom = atom(false);
export const activityUnreadAtom = atom(0);