diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index 0a176656..789c4f81 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -392,8 +392,12 @@ export class Ark { const seenIds = new Set(); const dedupQueue = new Set(); + const relayUrls = [...this.ndk.pool.relays.values()].map( + (item) => item.url, + ); + const events = await this.#fetcher.fetchLatestEvents( - this.#storage.account.relayList, + relayUrls, filter, limit, { diff --git a/packages/ark/src/components/note/primitives/repost.tsx b/packages/ark/src/components/note/primitives/repost.tsx index b4d7b666..6e8a90d9 100644 --- a/packages/ark/src/components/note/primitives/repost.tsx +++ b/packages/ark/src/components/note/primitives/repost.tsx @@ -28,6 +28,8 @@ export function RepostNote({ } }, refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, }); if (isLoading) { @@ -72,7 +74,6 @@ export function RepostNote({ - N diff --git a/packages/ark/src/components/note/user.tsx b/packages/ark/src/components/note/user.tsx index f693b339..ece9abe7 100644 --- a/packages/ark/src/components/note/user.tsx +++ b/packages/ark/src/components/note/user.tsx @@ -41,7 +41,7 @@ export function NoteUser({
@@ -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" /> ({ }); const LumeProvider = ({ children }: PropsWithChildren) => { + const queryClient = useQueryClient(); + const [context, setContext] = useState(undefined); const [isNewVersion, setIsNewVersion] = useState(false); @@ -176,14 +186,41 @@ const LumeProvider = ({ children }: PropsWithChildren) => { 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 diff --git a/packages/ark/tsconfig.json b/packages/ark/tsconfig.json index c785d0b8..34a32891 100644 --- a/packages/ark/tsconfig.json +++ b/packages/ark/tsconfig.json @@ -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"] } diff --git a/packages/icons/index.ts b/packages/icons/index.ts index 354f522e..c992993c 100644 --- a/packages/icons/index.ts +++ b/packages/icons/index.ts @@ -106,3 +106,4 @@ export * from "./src/check"; export * from "./src/popperFilled"; export * from "./src/composeFilled"; export * from "./src/settingsFilled"; +export * from "./src/bellFilled"; diff --git a/packages/icons/src/bell.tsx b/packages/icons/src/bell.tsx index 7b02ef20..48126612 100644 --- a/packages/icons/src/bell.tsx +++ b/packages/icons/src/bell.tsx @@ -1,20 +1,24 @@ -import { SVGProps } from 'react'; +import { SVGProps } from "react"; -export function BellIcon(props: JSX.IntrinsicAttributes & SVGProps) { - return ( - - - - ); +export function BellIcon( + props: JSX.IntrinsicAttributes & SVGProps, +) { + return ( + + + + ); } diff --git a/packages/icons/src/bellFilled.tsx b/packages/icons/src/bellFilled.tsx new file mode 100644 index 00000000..d1eb55d8 --- /dev/null +++ b/packages/icons/src/bellFilled.tsx @@ -0,0 +1,23 @@ +import { SVGProps } from "react"; + +export function BellFilledIcon( + props: JSX.IntrinsicAttributes & SVGProps, +) { + return ( + + + + ); +} diff --git a/packages/icons/src/chats.tsx b/packages/icons/src/chats.tsx index dc3fd919..ff6e08aa 100644 --- a/packages/icons/src/chats.tsx +++ b/packages/icons/src/chats.tsx @@ -1,21 +1,24 @@ -import { SVGProps } from 'react'; +import { SVGProps } from "react"; -export function ChatsIcon(props: JSX.IntrinsicAttributes & SVGProps) { - return ( - - - - ); +export function ChatsIcon( + props: JSX.IntrinsicAttributes & SVGProps, +) { + return ( + + + + ); } diff --git a/packages/ui/src/activity/column.tsx b/packages/ui/src/activity/column.tsx new file mode 100644 index 00000000..2555d890 --- /dev/null +++ b/packages/ui/src/activity/column.tsx @@ -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 ( + + {isActivityOpen ? ( + + + + ) : null} + + ); +} diff --git a/packages/ui/src/activity/content.tsx b/packages/ui/src/activity/content.tsx new file mode 100644 index 00000000..906c513c --- /dev/null +++ b/packages/ui/src/activity/content.tsx @@ -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 ; + case NDKKind.Repost: + return ; + case NDKKind.Zap: + return ; + default: + return ; + } + }, []); + + return ( +
+
+ {isLoading ? ( +
+ +
+ ) : allEvents.length < 1 ? ( +
+

🎉

+

Yo! Nothing new yet.

+
+ ) : ( + renderEvent(allEvents[0]) + )} +
+
+ + +
+
+ ); +} diff --git a/packages/ui/src/activity/reply.tsx b/packages/ui/src/activity/reply.tsx new file mode 100644 index 00000000..6fac4adf --- /dev/null +++ b/packages/ui/src/activity/reply.tsx @@ -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 ( +
+
+

+ Conversation +

+

+ @ Someone has replied to your note +

+
+
+ {thread ? ( +
+ + +
+ ) : null} +
+
+

New reply

+
+
+
+ + +
+ +
+ +
+ +
+
+ + +
+
+
+ ); +} diff --git a/packages/ui/src/activity/repost.tsx b/packages/ui/src/activity/repost.tsx new file mode 100644 index 00000000..87c657cb --- /dev/null +++ b/packages/ui/src/activity/repost.tsx @@ -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 ( +
+
+

Boost

+

+ @ Someone has reposted to your note +

+
+
+
+ +
+

Reposted

+
+
+
+
+
+ +
+
+
+ ); +} diff --git a/packages/ui/src/activity/rootNote.tsx b/packages/ui/src/activity/rootNote.tsx new file mode 100644 index 00000000..5ba7e589 --- /dev/null +++ b/packages/ui/src/activity/rootNote.tsx @@ -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 ( +
+
+
+
+
+ ); + } + + if (isError) { + return ( +
+
+ Failed to fetch event +
+
+ ); + } + + return ( + + +
+ +
+ +
+ +
+
+ + + ); +} diff --git a/packages/ui/src/activity/zap.tsx b/packages/ui/src/activity/zap.tsx new file mode 100644 index 00000000..3da017ff --- /dev/null +++ b/packages/ui/src/activity/zap.tsx @@ -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 ( +
+
+

Zap

+

+ @ Someone love your note +

+
+
+
+ +
+

Zapped

+
+
+
+
+
+ +
+
+
+ ); +} diff --git a/packages/ui/src/editor/form.tsx b/packages/ui/src/editor/form.tsx index e242dd03..61682e08 100644 --- a/packages/ui/src/editor/form.tsx +++ b/packages/ui/src/editor/form.tsx @@ -288,7 +288,7 @@ export function EditorForm() { }, [filters.length, editor, index, search, target]); return ( -
+
+
diff --git a/packages/ui/src/layouts/home.tsx b/packages/ui/src/layouts/home.tsx index 7d65efc4..bdab9273 100644 --- a/packages/ui/src/layouts/home.tsx +++ b/packages/ui/src/layouts/home.tsx @@ -5,7 +5,7 @@ export function HomeLayout() { return ( <> -
+
diff --git a/packages/ui/src/navigation.tsx b/packages/ui/src/navigation.tsx index a2f53228..9840c34a 100644 --- a/packages/ui/src/navigation.tsx +++ b/packages/ui/src/navigation.tsx @@ -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() {
)} - { + setIsActvityOpen((state) => !state); + setIsEditorOpen(false); + }} className="inline-flex flex-col items-center justify-center" > - {({ isActive }) => ( -
- {isActive ? ( - - ) : ( - - )} -
- )} -
- - {({ isActive }) => ( -
- {isActive ? ( - - ) : ( - - )} -
- )} -
+
+ {isActvityOpen ? ( + + ) : ( + + )} +
+ + + + + +
+ {fallbackName} +
+
+ ); + } + + return ( +
+ + + + {pubkey} + + +
+
+ {user?.name || + user?.display_name || + user?.displayName || + fallbackName} +
+

{subtext}

+
+
+ ); + } + if (variant === "notify") { if (isLoading) { return ( @@ -129,7 +176,7 @@ export const User = memo(function User({ @@ -525,105 +572,36 @@ export const User = memo(function User({ } return ( - -
- - - - - {pubkey} - - - -
-
- {user?.name || - user?.display_name || - user?.displayName || - fallbackName} -
-
-
- {createdAt} -
+
+ + + + {pubkey} + + +
+
+ {user?.name || + user?.display_name || + user?.displayName || + fallbackName} +
+
+
+ {createdAt}
- - -
- - - - {pubkey} - - -
-
-
- {user?.name || - user?.display_name || - user?.displayName || - user?.username} -
- {user?.nip05 ? ( - - ) : ( - - {fallbackName} - - )} -
-
-

- {user?.about} -

-
-
-
-
- - View profile - - - Message - -
-
-
- +
); }); diff --git a/packages/utils/src/state.ts b/packages/utils/src/state.ts index 5f30c483..a97bae4e 100644 --- a/packages/utils/src/state.ts +++ b/packages/utils/src/state.ts @@ -9,3 +9,6 @@ export const editorValueAtom = atom([ ]); export const onboardingAtom = atom(false); + +export const activityAtom = atom(false); +export const activityUnreadAtom = atom(0);