diff --git a/src/providers/index.tsx b/src/providers/index.tsx index 9639728bd..e85f7d2d4 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -4,6 +4,7 @@ import { SigningProvider } from "./signing-provider"; import createTheme from "../theme"; import useAppSettings from "../hooks/use-app-settings"; import { InvoiceModalProvider } from "./invoice-modal"; +import NotificationTimelineProvider from "./notification-timeline"; export const Providers = ({ children }: { children: React.ReactNode }) => { const { primaryColor } = useAppSettings(); @@ -12,7 +13,9 @@ export const Providers = ({ children }: { children: React.ReactNode }) => { return ( - {children} + + {children} + ); diff --git a/src/providers/notification-timeline.tsx b/src/providers/notification-timeline.tsx new file mode 100644 index 000000000..7d223aa62 --- /dev/null +++ b/src/providers/notification-timeline.tsx @@ -0,0 +1,50 @@ +import { PropsWithChildren, createContext, useContext, useEffect, useMemo } from "react"; +import { truncatedId } from "../helpers/nostr-event"; +import { useReadRelayUrls } from "../hooks/use-client-relays"; +import { useCurrentAccount } from "../hooks/use-current-account"; +import { TimelineLoader } from "../classes/timeline-loader"; +import timelineCacheService from "../services/timeline-cache"; +import { Kind } from "nostr-tools"; + +type NotificationTimelineContextType = { + timeline?: TimelineLoader; +}; +const NotificationTimelineContext = createContext({}); + +export function useNotificationTimeline() { + const context = useContext(NotificationTimelineContext); + + if (!context?.timeline) throw new Error("No notification timeline"); + + return context.timeline; +} + +export default function NotificationTimelineProvider({ children }: PropsWithChildren) { + const account = useCurrentAccount(); + const readRelays = useReadRelayUrls(); + + const timeline = useMemo(() => { + return account?.pubkey + ? timelineCacheService.createTimeline(`${truncatedId(account?.pubkey ?? "anon")}-notification`) + : undefined; + }, [account?.pubkey]); + + useEffect(() => { + if (timeline && account) { + timeline.setQuery([{ "#p": [account?.pubkey], kinds: [Kind.Text, Kind.Repost, Kind.Reaction, Kind.Zap] }]); + } + }, [account, timeline]); + + useEffect(() => { + timeline?.setRelays(readRelays); + }, [readRelays.join("|")]); + + useEffect(() => { + timeline?.open(); + return () => timeline?.close(); + }, [timeline]); + + const context = useMemo(() => ({ timeline }), [timeline]); + + return {children}; +} diff --git a/src/views/notifications/index.tsx b/src/views/notifications/index.tsx index f96c41635..130d67970 100644 --- a/src/views/notifications/index.tsx +++ b/src/views/notifications/index.tsx @@ -1,65 +1,119 @@ -import { memo, useCallback, useRef } from "react"; +import { memo, useCallback, useMemo, useRef } from "react"; import { Card, CardBody, CardHeader, Flex, Text } from "@chakra-ui/react"; import dayjs from "dayjs"; import { UserAvatar } from "../../components/user-avatar"; import { UserLink } from "../../components/user-link"; -import { useReadRelayUrls } from "../../hooks/use-client-relays"; import { useCurrentAccount } from "../../hooks/use-current-account"; -import { useTimelineLoader } from "../../hooks/use-timeline-loader"; import { NostrEvent } from "../../types/nostr-event"; import { NoteLink } from "../../components/note-link"; import RequireCurrentAccount from "../../providers/require-current-account"; import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer"; import useSubject from "../../hooks/use-subject"; -import { truncatedId } from "../../helpers/nostr-event"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; +import { useNotificationTimeline } from "../../providers/notification-timeline"; +import { Kind, getEventHash } from "nostr-tools"; +import { parseZapEvent } from "../../helpers/zaps"; +import { readablizeSats } from "../../helpers/bolt11"; +import { getReferences } from "../../helpers/nostr-event"; -const Kind1Notification = ({ event }: { event: NostrEvent }) => { - const ref = useRef(null); - useRegisterIntersectionEntity(ref, event.id); +const Kind1Notification = ({ event }: { event: NostrEvent }) => ( + + + + + + replied to your post + + {dayjs.unix(event.created_at).fromNow()} + + + + + {event.content.replace("\n", " ").slice(0, 64)} + + +); + +const ReactionNotification = ({ event }: { event: NostrEvent }) => { + const refs = getReferences(event); return ( - - - - - - - {dayjs.unix(event.created_at).fromNow()} - - - - - {event.content.replace("\n", " ").slice(0, 64)} - - + + + + reacted {event.content} to your post + + {dayjs.unix(event.created_at).fromNow()} + + + ); +}; + +const ZapNotification = ({ event }: { event: NostrEvent }) => { + const zap = useMemo(() => { + try { + return parseZapEvent(event); + } catch (e) {} + }, [event]); + + if (!zap || !zap.payment.amount) return null; + + return ( + + + + + zapped {readablizeSats(zap.payment.amount / 1000)} sats + {zap.eventId && ( + + {" "} + on note: + + )} + + + {dayjs.unix(zap.request.created_at).fromNow()} + + ); }; const NotificationItem = memo(({ event }: { event: NostrEvent }) => { - if (event.kind === 1) { - return ; + const ref = useRef(null); + useRegisterIntersectionEntity(ref, event.id); + + let content = Unknown event type {event.kind}; + switch (event.kind) { + case Kind.Text: + content = ; + break; + case Kind.Reaction: + content = ; + break; + case Kind.Zap: + content = ; + break; } - return <>Unknown event type {event.kind}; + + return
{content}
; }); function NotificationsPage() { - const readRelays = useReadRelayUrls(); const account = useCurrentAccount()!; const eventFilter = useCallback((event: NostrEvent) => event.pubkey !== account.pubkey, [account]); - const timeline = useTimelineLoader( - `${truncatedId(account.pubkey)}-notifications`, - readRelays, - { - "#p": [account.pubkey], - kinds: [1], - }, - { eventFilter } - ); + const timeline = useNotificationTimeline(); - const events = useSubject(timeline.timeline); + const events = useSubject(timeline?.timeline) ?? []; const scrollBox = useRef(null); const callback = useTimelineCurserIntersectionCallback(timeline);