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