rebuild notifications view

This commit is contained in:
hzrd149 2023-09-10 11:41:23 -05:00
parent 03fb66156d
commit b56156848c
14 changed files with 270 additions and 125 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Rebuild notifications view

View File

@ -21,7 +21,7 @@ export default function EmbeddedNote({ event, ...props }: Omit<CardProps, "child
<TrustProvider event={event}>
<Card {...props}>
<CardHeader padding="2" display="flex" gap="2" alignItems="center" flexWrap="wrap">
<UserAvatarLink pubkey={event.pubkey} size="sm" />
<UserAvatarLink pubkey={event.pubkey} size="xs" />
<UserLink pubkey={event.pubkey} fontWeight="bold" isTruncated fontSize="lg" />
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
<Button size="sm" onClick={expand.onToggle} leftIcon={expand.isOpen ? <ArrowUpSIcon /> : <ArrowDownSIcon />}>

View File

@ -0,0 +1,45 @@
import { Box, Card, CardProps, Divider, Flex, Link, Text } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { NostrEvent, isATag } from "../../../types/nostr-event";
import { UserLink } from "../../user-link";
import { UserAvatar } from "../../user-avatar";
import ChatMessageContent from "../../../views/streams/stream/stream-chat/chat-message-content";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
import { parseStreamEvent } from "../../../helpers/nostr/stream";
import StreamStatusBadge from "../../../views/streams/components/status-badge";
import { getSharableEventAddress } from "../../../helpers/nip19";
export default function EmbeddedStreamMessage({
message,
...props
}: Omit<CardProps, "children"> & { message: NostrEvent }) {
const streamCoordinate = message.tags.find(isATag)?.[1];
const streamEvent = useReplaceableEvent(streamCoordinate);
const stream = streamEvent && parseStreamEvent(streamEvent);
return (
<Card overflow="hidden" maxH="lg" display="block" p="2" {...props}>
{stream && (
<>
<Flex gap="2" alignItems="center">
<Link
as={RouterLink}
to={`/streams/${getSharableEventAddress(streamEvent) ?? ""}`}
fontWeight="bold"
fontSize="lg"
>
{stream.title}
</Link>
<StreamStatusBadge stream={stream} />
</Flex>
<Divider mb="2" />
</>
)}
<UserAvatar pubkey={message.pubkey} size="xs" display="inline-block" mr="2" />
<UserLink pubkey={message.pubkey} fontWeight="bold" />
<span>: </span>
<ChatMessageContent event={message} />
</Card>
);
}

View File

@ -8,7 +8,7 @@ import { NostrEvent } from "../../types/nostr-event";
import { Kind, nip19 } from "nostr-tools";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import RelayCard from "../../views/relays/components/relay-card";
import { STREAM_KIND } from "../../helpers/nostr/stream";
import { STREAM_CHAT_MESSAGE_KIND, STREAM_KIND } from "../../helpers/nostr/stream";
import { GOAL_KIND } from "../../helpers/nostr/goal";
import { safeDecode } from "../../helpers/nip19";
import EmbeddedStream from "./event-types/embedded-stream";
@ -20,6 +20,7 @@ import { NOTE_LIST_KIND, PEOPLE_LIST_KIND } from "../../helpers/nostr/lists";
import EmbeddedList from "./event-types/embedded-list";
import EmbeddedArticle from "./event-types/embedded-article";
import EmbeddedBadge from "./event-types/embedded-badge";
import EmbeddedStreamMessage from "./event-types/embedded-stream-message";
export type EmbedProps = {
goalProps?: EmbeddedGoalOptions;
@ -46,6 +47,8 @@ export function EmbedEvent({
return <EmbeddedArticle article={event} {...cardProps} />;
case Kind.BadgeDefinition:
return <EmbeddedBadge badge={event} {...cardProps} />;
case STREAM_CHAT_MESSAGE_KIND:
return <EmbeddedStreamMessage message={event} {...cardProps} />;
}
return <EmbeddedUnknown event={event} {...cardProps} />;

View File

@ -1,7 +1,7 @@
import { Button, ButtonProps, IconButton, useDisclosure } from "@chakra-ui/react";
import { readablizeSats } from "../../helpers/bolt11";
import { totalZaps } from "../../helpers/zaps";
import { totalZaps } from "../../helpers/nostr/zaps";
import { useCurrentAccount } from "../../hooks/use-current-account";
import useEventZaps from "../../hooks/use-event-zaps";
import clientRelaysService from "../../services/client-relays";

View File

@ -16,7 +16,7 @@ import { NostrEvent } from "../../types/nostr-event";
import { UserAvatarLink } from "../user-avatar-link";
import { UserLink } from "../user-link";
import { DislikeIcon, LightningIcon, LikeIcon } from "../icons";
import { ParsedZap } from "../../helpers/zaps";
import { ParsedZap } from "../../helpers/nostr/zaps";
import { readablizeSats } from "../../helpers/bolt11";
import useEventReactions from "../../hooks/use-event-reactions";
import useEventZaps from "../../hooks/use-event-zaps";

View File

@ -27,7 +27,7 @@ import { useSigningContext } from "../providers/signing-provider";
import appSettings from "../services/settings/app-settings";
import useSubject from "../hooks/use-subject";
import useUserLNURLMetadata from "../hooks/use-user-lnurl-metadata";
import { requestZapInvoice } from "../helpers/zaps";
import { requestZapInvoice } from "../helpers/nostr/zaps";
import { unique } from "../helpers/array";
import { useUserRelays } from "../hooks/use-user-relays";
import { RelayMode } from "../classes/relay";

View File

@ -1,8 +1,8 @@
import { bech32 } from "@scure/base";
import { isETag, isPTag, NostrEvent } from "../types/nostr-event";
import { ParsedInvoice, parsePaymentRequest } from "./bolt11";
import { isETag, isPTag, NostrEvent } from "../../types/nostr-event";
import { ParsedInvoice, parsePaymentRequest } from "../bolt11";
import { Kind0ParsedContent } from "./user-metadata";
import { Kind0ParsedContent } from "../user-metadata";
import { nip57, utils } from "nostr-tools";
// based on https://github.com/nbd-wtf/nostr-tools/blob/master/nip57.ts
@ -67,13 +67,10 @@ export function parseZapEvent(event: NostrEvent): ParsedZap {
const request = JSON.parse(zapRequestStr) as NostrEvent;
const payment = parsePaymentRequest(bolt11);
const eventId = request.tags.find(isETag)?.[1];
return {
event,
request,
payment,
eventId,
};
}

View File

@ -3,7 +3,7 @@ import { useMemo } from "react";
import eventZapsService from "../services/event-zaps";
import { useReadRelayUrls } from "./use-client-relays";
import useSubject from "./use-subject";
import { parseZapEvent } from "../helpers/zaps";
import { parseZapEvent } from "../helpers/nostr/zaps";
export default function useEventZaps(eventUID: string, additionalRelays: string[] = [], alwaysFetch = true) {
const relays = useReadRelayUrls(additionalRelays);

View File

@ -4,7 +4,7 @@ import { getGoalAmount, getGoalRelays } from "../../../helpers/nostr/goal";
import { LightningIcon } from "../../../components/icons";
import useEventZaps from "../../../hooks/use-event-zaps";
import { getEventUID } from "../../../helpers/nostr/events";
import { totalZaps } from "../../../helpers/zaps";
import { totalZaps } from "../../../helpers/nostr/zaps";
import { readablizeSats } from "../../../helpers/bolt11";
export default function GoalProgress({ goal }: { goal: NostrEvent }) {

View File

@ -1,11 +1,11 @@
import { memo, useCallback, useMemo, useRef } from "react";
import { Card, CardBody, CardHeader, Flex, Text } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { ReactNode, forwardRef, memo, useCallback, useMemo, useRef } from "react";
import { Box, Card, Flex, Switch, Text, useDisclosure } from "@chakra-ui/react";
import { Kind, nip18, nip25 } from "nostr-tools";
import { UserAvatar } from "../../components/user-avatar";
import { UserLink } from "../../components/user-link";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { NostrEvent } from "../../types/nostr-event";
import { NostrEvent, isATag, isETag } 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";
@ -13,45 +13,86 @@ import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../
import useSubject from "../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import { useNotificationTimeline } from "../../providers/notification-timeline";
import { parseZapEvent } from "../../helpers/zaps";
import { parseZapEvent } from "../../helpers/nostr/zaps";
import { readablizeSats } from "../../helpers/bolt11";
import { getEventUID, getReferences } from "../../helpers/nostr/events";
import { getEventUID, getReferences, parseCoordinate } from "../../helpers/nostr/events";
import Timestamp from "../../components/timestamp";
import { EmbedEvent, EmbedEventPointer } from "../../components/embed-event";
import EmbeddedUnknown from "../../components/embed-event/event-types/embedded-unknown";
import { NoteContents } from "../../components/note/note-contents";
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
const Kind1Notification = ({ event }: { event: NostrEvent }) => (
<Card size="sm" variant="outline">
<CardHeader>
<Flex gap="2" alignItems="center">
const Kind1Notification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
const refs = getReferences(event);
if (refs.replyId) {
return (
<Card variant="outline" p="2" borderColor="blue.400" ref={ref}>
<Flex gap="2" alignItems="center" mb="2">
<UserAvatar pubkey={event.pubkey} size="xs" />
<UserLink pubkey={event.pubkey} />
{refs.replyId ? <Text>replied to post</Text> : <Text>mentioned you</Text>}
<NoteLink noteId={event.id} color="current" ml="auto">
<Timestamp timestamp={event.created_at} />
</NoteLink>
</Flex>
{refs.replyId && <EmbedEventPointer pointer={{ type: "note", data: refs.replyId }} />}
<NoteContents event={event} mt="2" />
</Card>
);
}
return (
<Box ref={ref}>
<Flex gap="2" alignItems="center" mb="1">
<UserAvatar pubkey={event.pubkey} size="xs" />
<UserLink pubkey={event.pubkey} />
<Text>replied to your post</Text>
<Text>mentioned you in</Text>
</Flex>
<EmbedEvent event={event} />
</Box>
);
});
const ShareNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
const account = useCurrentAccount()!;
const pointer = nip18.getRepostedEventPointer(event);
if (pointer?.author !== account.pubkey) return null;
return (
<Card variant="outline" p="2" ref={ref}>
<Flex gap="2" alignItems="center" mb="2">
<UserAvatar pubkey={event.pubkey} size="xs" />
<UserLink pubkey={event.pubkey} />
<Text>shared note</Text>
<NoteLink noteId={event.id} color="current" ml="auto">
<Timestamp timestamp={event.created_at} />
</NoteLink>
</Flex>
</CardHeader>
<CardBody pt={0}>
<Text>{event.content.replace("\n", " ").slice(0, 64)}</Text>
</CardBody>
</Card>
);
{pointer && <EmbedEventPointer pointer={{ type: "nevent", data: pointer }} />}
</Card>
);
});
const ReactionNotification = ({ event }: { event: NostrEvent }) => {
const refs = getReferences(event);
const ReactionNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
const account = useCurrentAccount();
const pointer = nip25.getReactedEventPointer(event);
if (!pointer || (account?.pubkey && pointer.author !== account.pubkey)) return null;
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">
<Timestamp timestamp={event.created_at} />
</NoteLink>
</Flex>
<Box ref={ref}>
<Flex gap="2" alignItems="center" mb="1">
<UserAvatar pubkey={event.pubkey} size="xs" />
<UserLink pubkey={event.pubkey} />
<Text>reacted {event.content} to your post</Text>
<Timestamp timestamp={event.created_at} ml="auto" />
</Flex>
<EmbedEventPointer pointer={{ type: "nevent", data: pointer }} />
</Box>
);
};
});
const ZapNotification = ({ event }: { event: NostrEvent }) => {
const ZapNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
const zap = useMemo(() => {
try {
return parseZapEvent(event);
@ -60,65 +101,113 @@ const ZapNotification = ({ event }: { event: NostrEvent }) => {
if (!zap || !zap.payment.amount) return null;
const eventId = zap?.request.tags.find(isETag)?.[1];
const coordinate = zap?.request.tags.find(isATag)?.[1];
const parsedCoordinate = coordinate ? parseCoordinate(coordinate) : null;
let eventJSX: ReactNode | null = null;
if (parsedCoordinate && parsedCoordinate.identifier) {
eventJSX = (
<EmbedEventPointer
pointer={{
type: "naddr",
data: {
pubkey: parsedCoordinate.pubkey,
identifier: parsedCoordinate.identifier,
kind: parsedCoordinate.kind,
},
}}
/>
);
} else if (eventId) {
eventJSX = <EmbedEventPointer pointer={{ type: "note", data: eventId }} />;
}
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>
<Timestamp color="current" ml="auto" timestamp={zap.request.created_at} />
</Flex>
<Card variant="outline" borderColor="yellow.400" p="2" ref={ref}>
<Flex direction="row" gap="2" alignItems="center" mb="2">
<UserAvatar pubkey={zap.request.pubkey} size="xs" />
<UserLink pubkey={zap.request.pubkey} />
<Text>zapped {readablizeSats(zap.payment.amount / 1000)} sats</Text>
<Timestamp color="current" ml="auto" timestamp={zap.request.created_at} />
</Flex>
{eventJSX}
</Card>
);
};
});
const NotificationItem = memo(({ event }: { event: NostrEvent }) => {
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, getEventUID(event));
let content = <Text>Unknown event type {event.kind}</Text>;
switch (event.kind) {
case Kind.Text:
content = <Kind1Notification event={event} />;
break;
return <Kind1Notification event={event} ref={ref} />;
case Kind.Reaction:
content = <ReactionNotification event={event} />;
break;
return <ReactionNotification event={event} ref={ref} />;
case Kind.Repost:
return <ShareNotification event={event} ref={ref} />;
case Kind.Zap:
content = <ZapNotification event={event} />;
break;
return <ZapNotification event={event} ref={ref} />;
default:
return <EmbeddedUnknown event={event} />;
}
return <div ref={ref}>{content}</div>;
});
function NotificationsPage() {
const account = useCurrentAccount()!;
const hideReplies = useDisclosure();
const hideMentions = useDisclosure();
const hideZaps = useDisclosure();
const hideReactions = useDisclosure();
const hideShares = useDisclosure();
const { people } = usePeopleListContext();
const peoplePubkeys = useMemo(() => people?.map((p) => p.pubkey), [people]);
const eventFilter = useCallback(
(event: NostrEvent) => {
if (peoplePubkeys && event.kind !== Kind.Zap && !peoplePubkeys.includes(event.pubkey)) return false;
if (hideZaps.isOpen && event.kind === Kind.Zap) return false;
if (hideReactions.isOpen && event.kind === Kind.Reaction) return false;
if (hideShares.isOpen && event.kind === Kind.Repost) return false;
if (event.kind === Kind.Text) {
const refs = getReferences(event);
if (hideReplies.isOpen && refs.replyId) return false;
if (hideMentions.isOpen && !refs.replyId) return false;
}
return true;
},
[hideMentions.isOpen, hideReplies.isOpen, hideZaps.isOpen, hideReactions.isOpen, hideShares.isOpen, peoplePubkeys],
);
const eventFilter = useCallback((event: NostrEvent) => event.pubkey !== account.pubkey, [account]);
const timeline = useNotificationTimeline();
const events = useSubject(timeline?.timeline) ?? [];
const events = useSubject(timeline?.timeline).filter(eventFilter) ?? [];
const callback = useTimelineCurserIntersectionCallback(timeline);
return (
<IntersectionObserverProvider callback={callback}>
<Flex direction="column" gap="2">
<Flex gap="2" alignItems="center" py="2" wrap="wrap">
<PeopleListSelection />
<Switch isChecked={!hideReplies.isOpen} onChange={hideReplies.onToggle}>
Replies
</Switch>
<Switch isChecked={!hideMentions.isOpen} onChange={hideMentions.onToggle}>
Mentions
</Switch>
<Switch isChecked={!hideReactions.isOpen} onChange={hideReactions.onToggle}>
Reactions
</Switch>
<Switch isChecked={!hideShares.isOpen} onChange={hideShares.onToggle}>
Shares
</Switch>
<Switch isChecked={!hideZaps.isOpen} onChange={hideZaps.onToggle}>
Zaps
</Switch>
</Flex>
<Flex direction="column" gap="4" pt="2" pb="12">
{events.map((event) => (
<NotificationItem key={event.id} event={event} />
))}
@ -132,7 +221,9 @@ function NotificationsPage() {
export default function NotificationsView() {
return (
<RequireCurrentAccount>
<NotificationsPage />
<PeopleListProvider initList="global">
<NotificationsPage />
</PeopleListProvider>
</RequireCurrentAccount>
);
}

View File

@ -1,7 +1,7 @@
import { useMemo } from "react";
import { Flex, FlexProps, Text } from "@chakra-ui/react";
import { parseZapEvent } from "../../../helpers/zaps";
import { parseZapEvent } from "../../../helpers/nostr/zaps";
import { UserLink } from "../../../components/user-link";
import { LightningIcon } from "../../../components/icons";
import { readablizeSats } from "../../../helpers/bolt11";

View File

@ -7,7 +7,7 @@ import { UserLink } from "../../../../components/user-link";
import { NostrEvent } from "../../../../types/nostr-event";
import { useRegisterIntersectionEntity } from "../../../../providers/intersection-observer";
import { LightningIcon } from "../../../../components/icons";
import { parseZapEvent } from "../../../../helpers/zaps";
import { parseZapEvent } from "../../../../helpers/nostr/zaps";
import { readablizeSats } from "../../../../helpers/bolt11";
import { TrustProvider } from "../../../../providers/trust";
import ChatMessageContent from "./chat-message-content";

View File

@ -1,6 +1,6 @@
import { Box, Flex, Select, Text } from "@chakra-ui/react";
import dayjs from "dayjs";
import { useCallback, useMemo, useRef, useState } from "react";
import { ReactNode, useCallback, useMemo, useRef, useState } from "react";
import { useOutletContext } from "react-router-dom";
import { ErrorBoundary, ErrorFallback } from "../../components/error-boundary";
@ -9,9 +9,9 @@ import { NoteLink } from "../../components/note-link";
import { UserAvatarLink } from "../../components/user-avatar-link";
import { UserLink } from "../../components/user-link";
import { readablizeSats } from "../../helpers/bolt11";
import { isProfileZap, isNoteZap, parseZapEvent, totalZaps } from "../../helpers/zaps";
import { isProfileZap, isNoteZap, parseZapEvent, totalZaps } from "../../helpers/nostr/zaps";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { NostrEvent } from "../../types/nostr-event";
import { NostrEvent, isATag, isETag, isPTag } from "../../types/nostr-event";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
@ -21,55 +21,59 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types";
import Timestamp from "../../components/timestamp";
import { EmbedEventNostrLink, EmbedEventPointer } from "../../components/embed-event";
import { parseCoordinate } from "../../helpers/nostr/events";
const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, zapEvent.id);
try {
const { request, payment, eventId } = parseZapEvent(zapEvent);
const { request, payment } = parseZapEvent(zapEvent);
let embedContent: EmbedableContent = [request.content];
embedContent = embedNostrLinks(embedContent);
embedContent = embedUrls(embedContent, [renderGenericUrl]);
const eventId = request.tags.find(isETag)?.[1];
const coordinate = request.tags.find(isATag)?.[1];
const parsedCoordinate = coordinate ? parseCoordinate(coordinate) : null;
return (
<Box
borderWidth="1px"
borderRadius="lg"
overflow="hidden"
padding="2"
display="flex"
gap="2"
flexDirection="column"
flexShrink={0}
ref={ref}
>
<Flex gap="2" alignItems="center" wrap="wrap">
<UserAvatarLink pubkey={request.pubkey} size="xs" />
<UserLink pubkey={request.pubkey} />
<Text>Zapped</Text>
{eventId && <NoteLink noteId={eventId} />}
{payment.amount && (
<Flex gap="2">
<LightningIcon color="yellow.400" />
<Text>{readablizeSats(payment.amount / 1000)} sats</Text>
</Flex>
)}
<Timestamp ml="auto" timestamp={request.created_at} />
</Flex>
{embedContent && <Box>{embedContent}</Box>}
</Box>
let eventJSX: ReactNode | null = null;
if (parsedCoordinate && parsedCoordinate.identifier) {
eventJSX = (
<EmbedEventPointer
pointer={{
type: "naddr",
data: {
pubkey: parsedCoordinate.pubkey,
identifier: parsedCoordinate.identifier,
kind: parsedCoordinate.kind,
},
}}
/>
);
} catch (e) {
if (e instanceof Error) {
console.log(e);
return <ErrorFallback error={e} resetErrorBoundary={() => {}} />;
}
return null;
} else if (eventId) {
eventJSX = <EmbedEventPointer pointer={{ type: "note", data: eventId }} />;
}
let embedContent: EmbedableContent = [request.content];
embedContent = embedNostrLinks(embedContent);
embedContent = embedUrls(embedContent, [renderGenericUrl]);
return (
<Box ref={ref}>
<Flex gap="2" alignItems="center" wrap="wrap" mb="2">
<UserAvatarLink pubkey={request.pubkey} size="sm" />
<UserLink pubkey={request.pubkey} fontWeight="bold" />
<Text>Zapped</Text>
{payment.amount && (
<Flex gap="2">
<LightningIcon color="yellow.400" />
<Text>{readablizeSats(payment.amount / 1000)} sats</Text>
</Flex>
)}
<Timestamp ml="auto" timestamp={request.created_at} />
</Flex>
{embedContent && <Box>{embedContent}</Box>}
{eventJSX}
</Box>
);
};
const UserZapsTab = () => {