mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-27 12:07:43 +02:00
Show basic reactions and zaps in notifications
This commit is contained in:
@@ -4,6 +4,7 @@ import { SigningProvider } from "./signing-provider";
|
|||||||
import createTheme from "../theme";
|
import createTheme from "../theme";
|
||||||
import useAppSettings from "../hooks/use-app-settings";
|
import useAppSettings from "../hooks/use-app-settings";
|
||||||
import { InvoiceModalProvider } from "./invoice-modal";
|
import { InvoiceModalProvider } from "./invoice-modal";
|
||||||
|
import NotificationTimelineProvider from "./notification-timeline";
|
||||||
|
|
||||||
export const Providers = ({ children }: { children: React.ReactNode }) => {
|
export const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||||
const { primaryColor } = useAppSettings();
|
const { primaryColor } = useAppSettings();
|
||||||
@@ -12,7 +13,9 @@ export const Providers = ({ children }: { children: React.ReactNode }) => {
|
|||||||
return (
|
return (
|
||||||
<ChakraProvider theme={theme} colorModeManager={localStorageManager}>
|
<ChakraProvider theme={theme} colorModeManager={localStorageManager}>
|
||||||
<SigningProvider>
|
<SigningProvider>
|
||||||
<InvoiceModalProvider>{children}</InvoiceModalProvider>
|
<InvoiceModalProvider>
|
||||||
|
<NotificationTimelineProvider>{children}</NotificationTimelineProvider>
|
||||||
|
</InvoiceModalProvider>
|
||||||
</SigningProvider>
|
</SigningProvider>
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
);
|
);
|
||||||
|
50
src/providers/notification-timeline.tsx
Normal file
50
src/providers/notification-timeline.tsx
Normal file
@@ -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<NotificationTimelineContextType>({});
|
||||||
|
|
||||||
|
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 <NotificationTimelineContext.Provider value={context}>{children}</NotificationTimelineContext.Provider>;
|
||||||
|
}
|
@@ -1,30 +1,29 @@
|
|||||||
import { memo, useCallback, useRef } from "react";
|
import { memo, useCallback, useMemo, useRef } from "react";
|
||||||
import { Card, CardBody, CardHeader, Flex, Text } from "@chakra-ui/react";
|
import { Card, CardBody, CardHeader, Flex, Text } from "@chakra-ui/react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { UserAvatar } from "../../components/user-avatar";
|
import { UserAvatar } from "../../components/user-avatar";
|
||||||
import { UserLink } from "../../components/user-link";
|
import { UserLink } from "../../components/user-link";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { NoteLink } from "../../components/note-link";
|
import { NoteLink } from "../../components/note-link";
|
||||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||||
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { truncatedId } from "../../helpers/nostr-event";
|
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
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 Kind1Notification = ({ event }: { event: NostrEvent }) => (
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
<Card size="sm" variant="outline">
|
||||||
useRegisterIntersectionEntity(ref, event.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card size="sm" variant="outline" ref={ref}>
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<Flex gap="4" alignItems="center">
|
<Flex gap="2" alignItems="center">
|
||||||
<UserAvatar pubkey={event.pubkey} size="sm" />
|
<UserAvatar pubkey={event.pubkey} size="xs" />
|
||||||
<UserLink pubkey={event.pubkey} />
|
<UserLink pubkey={event.pubkey} />
|
||||||
|
<Text>replied to your post</Text>
|
||||||
<NoteLink noteId={event.id} color="current" ml="auto">
|
<NoteLink noteId={event.id} color="current" ml="auto">
|
||||||
{dayjs.unix(event.created_at).fromNow()}
|
{dayjs.unix(event.created_at).fromNow()}
|
||||||
</NoteLink>
|
</NoteLink>
|
||||||
@@ -34,32 +33,87 @@ const Kind1Notification = ({ event }: { event: NostrEvent }) => {
|
|||||||
<Text>{event.content.replace("\n", " ").slice(0, 64)}</Text>
|
<Text>{event.content.replace("\n", " ").slice(0, 64)}</Text>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ReactionNotification = ({ event }: { event: NostrEvent }) => {
|
||||||
|
const refs = getReferences(event);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex gap="2" alignItems="center" px="2">
|
||||||
|
<UserAvatar pubkey={event.pubkey} size="xs" />
|
||||||
|
<UserLink pubkey={event.pubkey} />
|
||||||
|
<Text>reacted {event.content} to your post</Text>
|
||||||
|
<NoteLink noteId={refs.replyId || event.id} color="current" ml="auto">
|
||||||
|
{dayjs.unix(event.created_at).fromNow()}
|
||||||
|
</NoteLink>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZapNotification = ({ event }: { event: NostrEvent }) => {
|
||||||
|
const zap = useMemo(() => {
|
||||||
|
try {
|
||||||
|
return parseZapEvent(event);
|
||||||
|
} catch (e) {}
|
||||||
|
}, [event]);
|
||||||
|
|
||||||
|
if (!zap || !zap.payment.amount) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
direction="row"
|
||||||
|
borderRadius="md"
|
||||||
|
borderColor="yellow.400"
|
||||||
|
borderWidth="1px"
|
||||||
|
p="2"
|
||||||
|
gap="2"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<UserAvatar pubkey={zap.request.pubkey} size="xs" />
|
||||||
|
<UserLink pubkey={zap.request.pubkey} />
|
||||||
|
<Text>
|
||||||
|
zapped {readablizeSats(zap.payment.amount / 1000)} sats
|
||||||
|
{zap.eventId && (
|
||||||
|
<span>
|
||||||
|
{" "}
|
||||||
|
on note: <NoteLink noteId={zap.eventId} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Text color="current" ml="auto">
|
||||||
|
{dayjs.unix(zap.request.created_at).fromNow()}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NotificationItem = memo(({ event }: { event: NostrEvent }) => {
|
const NotificationItem = memo(({ event }: { event: NostrEvent }) => {
|
||||||
if (event.kind === 1) {
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
return <Kind1Notification event={event} />;
|
useRegisterIntersectionEntity(ref, event.id);
|
||||||
|
|
||||||
|
let content = <Text>Unknown event type {event.kind}</Text>;
|
||||||
|
switch (event.kind) {
|
||||||
|
case Kind.Text:
|
||||||
|
content = <Kind1Notification event={event} />;
|
||||||
|
break;
|
||||||
|
case Kind.Reaction:
|
||||||
|
content = <ReactionNotification event={event} />;
|
||||||
|
break;
|
||||||
|
case Kind.Zap:
|
||||||
|
content = <ZapNotification event={event} />;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return <>Unknown event type {event.kind}</>;
|
|
||||||
|
return <div ref={ref}>{content}</div>;
|
||||||
});
|
});
|
||||||
|
|
||||||
function NotificationsPage() {
|
function NotificationsPage() {
|
||||||
const readRelays = useReadRelayUrls();
|
|
||||||
const account = useCurrentAccount()!;
|
const account = useCurrentAccount()!;
|
||||||
|
|
||||||
const eventFilter = useCallback((event: NostrEvent) => event.pubkey !== account.pubkey, [account]);
|
const eventFilter = useCallback((event: NostrEvent) => event.pubkey !== account.pubkey, [account]);
|
||||||
const timeline = useTimelineLoader(
|
const timeline = useNotificationTimeline();
|
||||||
`${truncatedId(account.pubkey)}-notifications`,
|
|
||||||
readRelays,
|
|
||||||
{
|
|
||||||
"#p": [account.pubkey],
|
|
||||||
kinds: [1],
|
|
||||||
},
|
|
||||||
{ eventFilter }
|
|
||||||
);
|
|
||||||
|
|
||||||
const events = useSubject(timeline.timeline);
|
const events = useSubject(timeline?.timeline) ?? [];
|
||||||
|
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
Reference in New Issue
Block a user