diff --git a/.changeset/rotten-cycles-teach.md b/.changeset/rotten-cycles-teach.md new file mode 100644 index 000000000..288dc8373 --- /dev/null +++ b/.changeset/rotten-cycles-teach.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Rebuild notifications view diff --git a/src/classes/pubkey-subject-cache.ts b/src/classes/pubkey-subject-cache.ts deleted file mode 100644 index 9af28c8e4..000000000 --- a/src/classes/pubkey-subject-cache.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Subject from "./subject"; - -/** @deprecated */ -export class PubkeySubjectCache { - subjects = new Map>(); - relays = new Map>(); - dirty = false; - - hasSubject(pubkey: string) { - return this.subjects.has(pubkey); - } - getSubject(pubkey: string) { - let subject = this.subjects.get(pubkey); - if (!subject) { - subject = new Subject(null); - this.subjects.set(pubkey, subject); - this.dirty = true; - } - return subject; - } - addRelays(pubkey: string, relays: string[]) { - const set = this.relays.get(pubkey) ?? new Set(); - for (const url of relays) set.add(url); - this.relays.set(pubkey, set); - this.dirty = true; - } - - getAllPubkeysMissingData(include: string[] = []) { - const pubkeys: string[] = []; - const relays = new Set(); - - for (const [pubkey, subject] of this.subjects) { - if (subject.value === null || include.includes(pubkey)) { - pubkeys.push(pubkey); - const r = this.relays.get(pubkey); - if (r) { - for (const url of r) relays.add(url); - } - } - } - return { pubkeys, relays: Array.from(relays) }; - } - - prune() { - const prunedKeys: string[] = []; - for (const [key, subject] of this.subjects) { - if (!subject.hasListeners) { - this.subjects.delete(key); - this.relays.delete(key); - prunedKeys.push(key); - this.dirty = true; - } - } - return prunedKeys; - } -} diff --git a/src/classes/timeline-loader.ts b/src/classes/timeline-loader.ts index a402f87e5..2bac4f38c 100644 --- a/src/classes/timeline-loader.ts +++ b/src/classes/timeline-loader.ts @@ -22,7 +22,7 @@ const BLOCK_SIZE = 30; export type EventFilter = (event: NostrEvent, store: EventStore) => boolean; -export class RelayTimelineLoader { +export class RelayBlockLoader { relay: string; query: NostrRequestFilter; blockSize = BLOCK_SIZE; @@ -53,7 +53,7 @@ export class RelayTimelineLoader { query = addToQuery(query, { until: oldestEvent.created_at - 1 }); } - const request = new NostrRequest([this.relay], 20 * 1000); + const request = new NostrRequest([this.relay]); let gotEvents = 0; request.onEvent.subscribe((e) => { @@ -89,11 +89,11 @@ export class RelayTimelineLoader { deleteEventService.stream.unsubscribe(this.handleDeleteEvent, this); } - getFirstEvent(nth = 0, filter?: EventFilter) { - return this.events.getFirstEvent(nth, filter); + getFirstEvent(nth = 0, eventFilter?: EventFilter) { + return this.events.getFirstEvent(nth, eventFilter); } - getLastEvent(nth = 0, filter?: EventFilter) { - return this.events.getLastEvent(nth, filter); + getLastEvent(nth = 0, eventFilter?: EventFilter) { + return this.events.getLastEvent(nth, eventFilter); } } @@ -114,7 +114,7 @@ export default class TimelineLoader { private log: Debugger; private subscription: NostrMultiSubscription; - relayTimelineLoaders = new Map(); + blockLoaders = new Map(); constructor(name: string) { this.name = name; @@ -153,27 +153,27 @@ export default class TimelineLoader { if (eventId) this.events.deleteEvent(eventId); } - private createLoaders() { + private createBlockLoaders() { if (!this.query) return; for (const relay of this.relays) { - if (!this.relayTimelineLoaders.has(relay)) { - const loader = new RelayTimelineLoader(relay, this.query, this.log.extend(relay)); - this.relayTimelineLoaders.set(relay, loader); + if (!this.blockLoaders.has(relay)) { + const loader = new RelayBlockLoader(relay, this.query, this.log.extend(relay)); + this.blockLoaders.set(relay, loader); this.events.connect(loader.events); loader.onBlockFinish.subscribe(this.updateLoading, this); loader.onBlockFinish.subscribe(this.updateComplete, this); } } } - private removeLoaders(filter?: (loader: RelayTimelineLoader) => boolean) { - for (const [relay, loader] of this.relayTimelineLoaders) { + private removeBlockLoaders(filter?: (loader: RelayBlockLoader) => boolean) { + for (const [relay, loader] of this.blockLoaders) { if (!filter || filter(loader)) { loader.cleanup(); this.events.disconnect(loader.events); loader.onBlockFinish.unsubscribe(this.updateLoading, this); loader.onBlockFinish.unsubscribe(this.updateComplete, this); - this.relayTimelineLoaders.delete(relay); + this.blockLoaders.delete(relay); } } } @@ -182,10 +182,10 @@ export default class TimelineLoader { if (this.relays.sort().join("|") === relays.sort().join("|")) return; // remove loaders - this.removeLoaders((loader) => !relays.includes(loader.relay)); + this.removeBlockLoaders((loader) => !relays.includes(loader.relay)); this.relays = relays; - this.createLoaders(); + this.createBlockLoaders(); this.subscription.setRelays(relays); this.updateComplete(); @@ -194,7 +194,7 @@ export default class TimelineLoader { if (JSON.stringify(this.query) === JSON.stringify(query)) return; // remove all loaders - this.removeLoaders(); + this.removeBlockLoaders(); this.log("set query", query); this.query = query; @@ -202,7 +202,7 @@ export default class TimelineLoader { // forget all events this.forgetEvents(); // create any missing loaders - this.createLoaders(); + this.createBlockLoaders(); // update the complete flag this.updateComplete(); // update the subscription with the new query @@ -220,7 +220,7 @@ export default class TimelineLoader { loadNextBlocks() { let triggeredLoad = false; - for (const [relay, loader] of this.relayTimelineLoaders) { + for (const [relay, loader] of this.blockLoaders) { if (loader.complete || loader.loading) continue; const event = loader.getLastEvent(this.loadNextBlockBuffer, this.eventFilter); if (!event || event.created_at >= this.cursor) { @@ -233,7 +233,7 @@ export default class TimelineLoader { /** @deprecated */ loadMore() { let triggeredLoad = false; - for (const [relay, loader] of this.relayTimelineLoaders) { + for (const [relay, loader] of this.blockLoaders) { if (loader.complete || loader.loading) continue; loader.loadNextBlock(); triggeredLoad = true; @@ -242,7 +242,7 @@ export default class TimelineLoader { } private updateLoading() { - for (const [relay, loader] of this.relayTimelineLoaders) { + for (const [relay, loader] of this.blockLoaders) { if (loader.loading) { if (!this.loading.value) { this.loading.next(true); @@ -253,7 +253,7 @@ export default class TimelineLoader { if (this.loading.value) this.loading.next(false); } private updateComplete() { - for (const [relay, loader] of this.relayTimelineLoaders) { + for (const [relay, loader] of this.blockLoaders) { if (!loader.complete) { this.complete.next(false); return; @@ -270,14 +270,14 @@ export default class TimelineLoader { reset() { this.cursor = dayjs().unix(); - this.removeLoaders(); + this.removeBlockLoaders(); this.forgetEvents(); } /** close the subscription and remove any event listeners for this timeline */ cleanup() { this.close(); - this.removeLoaders(); + this.removeBlockLoaders(); this.events.cleanup(); deleteEventService.stream.unsubscribe(this.handleDeleteEvent, this); } diff --git a/src/components/embed-event/event-types/embedded-note.tsx b/src/components/embed-event/event-types/embedded-note.tsx index 95564bc37..cd694aa75 100644 --- a/src/components/embed-event/event-types/embedded-note.tsx +++ b/src/components/embed-event/event-types/embedded-note.tsx @@ -13,7 +13,7 @@ import { TrustProvider } from "../../../providers/trust"; import { NoteLink } from "../../note-link"; import Timestamp from "../../timestamp"; import { getSharableEventAddress } from "../../../helpers/nip19"; -import { InlineNoteContent } from "../../note/inline-note-content"; +import { InlineNoteContent } from "../../inline-note-content"; import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider"; import HoverLinkOverlay from "../../hover-link-overlay"; diff --git a/src/components/embed-event/event-types/embedded-stemstr-track.tsx b/src/components/embed-event/event-types/embedded-stemstr-track.tsx index 98ba5a5a7..2a9d2746c 100644 --- a/src/components/embed-event/event-types/embedded-stemstr-track.tsx +++ b/src/components/embed-event/event-types/embedded-stemstr-track.tsx @@ -18,7 +18,7 @@ import { import { NostrEvent } from "../../../types/nostr-event"; import UserAvatarLink from "../../user-avatar-link"; import { UserLink } from "../../user-link"; -import { InlineNoteContent } from "../../note/inline-note-content"; +import { InlineNoteContent } from "../../inline-note-content"; import { getDownloadURL, getHashtags, getStreamURL } from "../../../helpers/nostr/stemstr"; import { DownloadIcon, ReplyIcon } from "../../icons"; import NoteZapButton from "../../note/note-zap-button"; diff --git a/src/components/note/inline-note-content.tsx b/src/components/inline-note-content.tsx similarity index 84% rename from src/components/note/inline-note-content.tsx rename to src/components/inline-note-content.tsx index 569222162..5ebad39ca 100644 --- a/src/components/note/inline-note-content.tsx +++ b/src/components/inline-note-content.tsx @@ -1,10 +1,10 @@ import React from "react"; import { Box, BoxProps } from "@chakra-ui/react"; -import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event"; -import { EmbedableContent, embedUrls, truncateEmbedableContent } from "../../helpers/embeds"; -import { embedNostrLinks, embedNostrMentions, embedNostrHashtags, embedEmoji, renderGenericUrl } from "../embed-types"; -import { LightboxProvider } from "../lightbox-provider"; +import { DraftNostrEvent, NostrEvent } from "../types/nostr-event"; +import { EmbedableContent, embedUrls, truncateEmbedableContent } from "../helpers/embeds"; +import { embedNostrLinks, embedNostrMentions, embedNostrHashtags, embedEmoji, renderGenericUrl } from "./embed-types"; +import { LightboxProvider } from "./lightbox-provider"; function buildContents(event: NostrEvent | DraftNostrEvent) { let content: EmbedableContent = [event.content.trim().replace(/\n+/g, "\n")]; diff --git a/src/components/note/index.tsx b/src/components/note/index.tsx index 165ef74ed..254167aab 100644 --- a/src/components/note/index.tsx +++ b/src/components/note/index.tsx @@ -46,7 +46,7 @@ import HoverLinkOverlay from "../hover-link-overlay"; import { nip19 } from "nostr-tools"; import NoteCommunityMetadata from "./note-community-metadata"; import useSingleEvent from "../../hooks/use-single-event"; -import { InlineNoteContent } from "./inline-note-content"; +import { InlineNoteContent } from "../inline-note-content"; import NoteProxyLink from "./components/note-proxy-link"; import { NoteDetailsButton } from "./components/note-details-button"; import EventInteractionDetailsModal from "../event-interactions-modal"; diff --git a/src/helpers/nostr/events.ts b/src/helpers/nostr/events.ts index 946d1468e..7fd076254 100644 --- a/src/helpers/nostr/events.ts +++ b/src/helpers/nostr/events.ts @@ -6,6 +6,7 @@ import { RelayConfig, RelayMode } from "../../classes/relay"; import { getMatchNostrLink } from "../regexp"; import { AddressPointer } from "nostr-tools/lib/types/nip19"; import { safeJson } from "../parse"; +import { getContentMentions } from "./post"; export function truncatedId(str: string, keep = 6) { if (str.length < keep * 2 + 3) return str; @@ -28,6 +29,9 @@ export function getEventUID(event: NostrEvent) { export function isReply(event: NostrEvent | DraftNostrEvent) { return event.kind === 1 && !!getReferences(event).replyId; } +export function isMentionedInContent(event: NostrEvent | DraftNostrEvent, pubkey: string) { + return event.kind === 1 && filterTagsByContentRefs(event.content, event.tags).some((t) => t[1] === pubkey); +} export function isRepost(event: NostrEvent | DraftNostrEvent) { const match = event.content.match(getMatchNostrLink()); diff --git a/src/providers/notification-timeline.tsx b/src/providers/notification-timeline.tsx index b4962f90f..3bb6b81f2 100644 --- a/src/providers/notification-timeline.tsx +++ b/src/providers/notification-timeline.tsx @@ -1,12 +1,12 @@ -import { PropsWithChildren, createContext, useCallback, useContext, useEffect, useMemo } from "react"; +import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react"; import { Kind } from "nostr-tools"; 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 { NostrEvent } from "../types/nostr-event"; import useClientSideMuteFilter from "../hooks/use-client-side-mute-filter"; +import useTimelineLoader from "../hooks/use-timeline-loader"; type NotificationTimelineContextType = { timeline?: TimelineLoader; @@ -25,12 +25,6 @@ export default function NotificationTimelineProvider({ children }: PropsWithChil const account = useCurrentAccount(); const readRelays = useReadRelayUrls(); - const timeline = useMemo(() => { - return account?.pubkey - ? timelineCacheService.createTimeline(`${account?.pubkey ?? "anon"}-notification`) - : undefined; - }, [account?.pubkey]); - const userMuteFilter = useClientSideMuteFilter(); const eventFilter = useCallback( (event: NostrEvent) => { @@ -39,24 +33,13 @@ export default function NotificationTimelineProvider({ children }: PropsWithChil }, [userMuteFilter], ); - useEffect(() => { - timeline?.setFilter(eventFilter); - }, [timeline, eventFilter]); - useEffect(() => { - if (timeline && account?.pubkey) { - timeline.setQuery([{ "#p": [account?.pubkey], kinds: [Kind.Text, Kind.Repost, Kind.Reaction, Kind.Zap] }]); - } - }, [account?.pubkey, timeline]); - - useEffect(() => { - timeline?.setRelays(readRelays); - }, [readRelays.join("|")]); - - useEffect(() => { - timeline?.open(); - return () => timeline?.close(); - }, [timeline]); + const timeline = useTimelineLoader( + `${account?.pubkey ?? "anon"}-notification`, + readRelays, + { "#p": [account?.pubkey ?? "0000"], kinds: [Kind.Text, Kind.Repost, Kind.Reaction, Kind.Zap] }, + { enabled: !!account?.pubkey, eventFilter }, + ); const context = useMemo(() => ({ timeline }), [timeline]); diff --git a/src/views/community/components/community-post.tsx b/src/views/community/components/community-post.tsx index 9de4b2ed6..68484e0a2 100644 --- a/src/views/community/components/community-post.tsx +++ b/src/views/community/components/community-post.tsx @@ -21,7 +21,7 @@ import { getEventCommunityPointer, getPostSubject } from "../../../helpers/nostr import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider"; import { getSharableEventAddress } from "../../../helpers/nip19"; import HoverLinkOverlay from "../../../components/hover-link-overlay"; -import { InlineNoteContent } from "../../../components/note/inline-note-content"; +import { InlineNoteContent } from "../../../components/inline-note-content"; import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer"; import { getEventUID, parseHardcodedNoteContent } from "../../../helpers/nostr/events"; import { UserLink } from "../../../components/user-link"; diff --git a/src/views/notifications/index.tsx b/src/views/notifications/index.tsx index 8b94d3f68..6cb9fd3ac 100644 --- a/src/views/notifications/index.tsx +++ b/src/views/notifications/index.tsx @@ -1,145 +1,64 @@ -import { useCallback, useMemo } from "react"; -import { Tab, TabList, TabPanel, TabPanelProps, TabPanels, Tabs, useDisclosure } from "@chakra-ui/react"; +import { useMemo } from "react"; +import { Flex, useDisclosure } from "@chakra-ui/react"; import { Kind } from "nostr-tools"; -import { NostrEvent } from "../../types/nostr-event"; import RequireCurrentAccount from "../../providers/require-current-account"; import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; import IntersectionObserverProvider from "../../providers/intersection-observer"; import useSubject from "../../hooks/use-subject"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import { useNotificationTimeline } from "../../providers/notification-timeline"; -import { getReferences } from "../../helpers/nostr/events"; +import { isReply } from "../../helpers/nostr/events"; import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider"; import PeopleListSelection from "../../components/people-list-selection/people-list-selection"; import VerticalPageLayout from "../../components/vertical-page-layout"; import NotificationItem from "./notification-item"; - -function RepliesNotificationsTab({ events }: { events: NostrEvent[] }) { - const timeline = useNotificationTimeline(); - const filtered = events.filter((event) => { - if (event.kind === Kind.Text) { - const refs = getReferences(event); - return !!refs.replyId; - } - return false; - }); - - return ( - <> - {filtered.map((event) => ( - - ))} - - - ); -} - -function MentionsNotificationsTab({ events }: { events: NostrEvent[] }) { - const timeline = useNotificationTimeline(); - const filtered = events.filter((event) => { - if (event.kind === Kind.Text) { - const refs = getReferences(event); - return !refs.replyId; - } - return false; - }); - - return ( - <> - {filtered.map((event) => ( - - ))} - - - ); -} - -function ReactionsNotificationsTab({ events }: { events: NostrEvent[] }) { - const timeline = useNotificationTimeline(); - const filtered = events.filter((e) => e.kind === Kind.Reaction); - - return ( - <> - {filtered.map((event) => ( - - ))} - - - ); -} - -function SharesNotificationsTab({ events }: { events: NostrEvent[] }) { - const timeline = useNotificationTimeline(); - const filtered = events.filter((e) => e.kind === Kind.Repost); - - return ( - <> - {filtered.map((event) => ( - - ))} - - - ); -} - -function ZapNotificationsTab({ events }: { events: NostrEvent[] }) { - const timeline = useNotificationTimeline(); - const filtered = events.filter((e) => e.kind === Kind.Zap); - - return ( - <> - {filtered.map((event) => ( - - ))} - - - ); -} +import NotificationTypeToggles from "./notification-type-toggles"; function NotificationsPage() { const { people } = usePeopleListContext(); const peoplePubkeys = useMemo(() => people?.map((p) => p.pubkey), [people]); + const showReplies = useDisclosure({ defaultIsOpen: true }); + const showMentions = useDisclosure({ defaultIsOpen: true }); + const showZaps = useDisclosure({ defaultIsOpen: true }); + const showReposts = useDisclosure({ defaultIsOpen: true }); + const showReactions = useDisclosure({ defaultIsOpen: true }); + const timeline = useNotificationTimeline(); const callback = useTimelineCurserIntersectionCallback(timeline); const events = useSubject(timeline?.timeline).filter((e) => { if (peoplePubkeys && e.kind !== Kind.Zap && !peoplePubkeys.includes(e.pubkey)) return false; + + if (e.kind === Kind.Text) { + if (!showReplies.isOpen && isReply(e)) return false; + if (!showMentions.isOpen && !isReply(e)) return false; + } + if (!showReactions.isOpen && e.kind === Kind.Reaction) return false; + if (!showReposts.isOpen && e.kind === Kind.Repost) return false; + if (!showZaps.isOpen && e.kind === Kind.Zap) return false; + return true; }); - const tabPanelProps: TabPanelProps = { px: "0", pt: "2", display: "flex", flexDirection: "column", gap: "2" }; - return ( - - - Replies - Mentions - Reactions - Shares - Zaps - - - - - - - - - - - - - - - - - - - - + + + + + + {events.map((event) => ( + + ))} + ); diff --git a/src/views/notifications/notification-item.tsx b/src/views/notifications/notification-item.tsx index efdbd7f8e..7121405b3 100644 --- a/src/views/notifications/notification-item.tsx +++ b/src/views/notifications/notification-item.tsx @@ -1,89 +1,119 @@ -import { ReactNode, forwardRef, memo, useMemo, useRef } from "react"; -import { Box, Card, Flex, Text } from "@chakra-ui/react"; +import { PropsWithChildren, ReactNode, forwardRef, memo, useMemo, useRef } from "react"; +import { AvatarGroup, Box, Flex, IconButton, IconButtonProps, 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, isATag, isETag } from "../../types/nostr-event"; -import { NoteLink } from "../../components/note-link"; import { useRegisterIntersectionEntity } from "../../providers/intersection-observer"; import { parseZapEvent } from "../../helpers/nostr/zaps"; import { readablizeSats } from "../../helpers/bolt11"; -import { getEventUID, getReferences, parseCoordinate } from "../../helpers/nostr/events"; -import Timestamp from "../../components/timestamp"; +import { getEventUID, getReferences, isMentionedInContent, isReply, parseCoordinate } from "../../helpers/nostr/events"; import { EmbedEvent, EmbedEventPointer } from "../../components/embed-event"; import EmbeddedUnknown from "../../components/embed-event/event-types/embedded-unknown"; -import { NoteContents } from "../../components/note/text-note-contents"; import { ErrorBoundary } from "../../components/error-boundary"; import { TrustProvider } from "../../providers/trust"; +import Heart from "../../components/icons/heart"; +import UserAvatarLink from "../../components/user-avatar-link"; +import { AtIcon, ChevronDownIcon, ChevronUpIcon, LightningIcon, ReplyIcon, RepostIcon } from "../../components/icons"; +import useSingleEvent from "../../hooks/use-single-event"; -const Kind1Notification = forwardRef(({ event }, ref) => { +const IconBox = ({ children }: PropsWithChildren) => ( + + {children} + +); +const ExpandableToggleButton = ({ + toggle, + ...props +}: { toggle: { isOpen: boolean; onToggle: () => void } } & Omit) => ( + : } + variant="ghost" + onClick={toggle.onToggle} + {...props} + /> +); + +const ReplyNotification = forwardRef(({ event }, ref) => { + const account = useCurrentAccount(); const refs = getReferences(event); + const parent = useSingleEvent(refs.replyId); + + if (!refs.replyId || (parent && parent.pubkey !== account?.pubkey)) return null; - if (refs.replyId) { - return ( - - - - - {refs.replyId ? replied to: : mentioned you} - - - - - - - - ); - } return ( - - - - - mentioned you in + + + + + + - - + ); }); -const ShareNotification = forwardRef(({ event }, ref) => { +const MentionNotification = forwardRef(({ event }, ref) => { + return ( + + + + + + + + + ); +}); + +const RepostNotification = forwardRef(({ event }, ref) => { const account = useCurrentAccount()!; const pointer = nip18.getRepostedEventPointer(event); + const expanded = useDisclosure({ defaultIsOpen: true }); + if (pointer?.author !== account.pubkey) return null; return ( - - - - - shared note: - - - + + + + + + + + + + + + {expanded.isOpen && } - {pointer && } - + ); }); const ReactionNotification = forwardRef(({ event }, ref) => { const account = useCurrentAccount(); const pointer = nip25.getReactedEventPointer(event); + const expanded = useDisclosure({ defaultIsOpen: true }); if (!pointer || (account?.pubkey && pointer.author !== account.pubkey)) return null; return ( - - - - - reacted {event.content} to your post - + + + + + + + + + + {event.content} + + {/* */} + + {expanded.isOpen && } - - + ); }); @@ -99,6 +129,7 @@ const ZapNotification = forwardRef(({ eve const eventId = zap?.request.tags.find(isETag)?.[1]; const coordinate = zap?.request.tags.find(isATag)?.[1]; const parsedCoordinate = coordinate ? parseCoordinate(coordinate) : null; + const expanded = useDisclosure({ defaultIsOpen: true }); let eventJSX: ReactNode | null = null; if (parsedCoordinate && parsedCoordinate.identifier) { @@ -119,32 +150,42 @@ const ZapNotification = forwardRef(({ eve } return ( - - - - - zapped {readablizeSats(zap.payment.amount / 1000)} sats - + + + + + + + + + + {readablizeSats(zap.payment.amount / 1000)} sats + + {/* */} + + {expanded.isOpen && eventJSX} - {eventJSX} - + ); }); const NotificationItem = ({ event }: { event: NostrEvent }) => { + const account = useCurrentAccount(); const ref = useRef(null); useRegisterIntersectionEntity(ref, getEventUID(event)); let content: ReactNode | null = null; switch (event.kind) { case Kind.Text: - content = ; + if (isReply(event)) content = ; + else if (account?.pubkey && isMentionedInContent(event, account.pubkey)) + content = ; break; case Kind.Reaction: content = ; break; case Kind.Repost: - content = ; + content = ; break; case Kind.Zap: content = ; diff --git a/src/views/notifications/notification-type-toggles.tsx b/src/views/notifications/notification-type-toggles.tsx new file mode 100644 index 000000000..1becf2bb9 --- /dev/null +++ b/src/views/notifications/notification-type-toggles.tsx @@ -0,0 +1,61 @@ +import { ButtonGroup, ButtonGroupProps, IconButton, IconButtonProps } from "@chakra-ui/react"; +import { AtIcon, LightningIcon, ReplyIcon, RepostIcon } from "../../components/icons"; +import Heart from "../../components/icons/heart"; + +type Disclosure = { isOpen: boolean; onToggle: () => void }; + +function ToggleIconButton({ toggle, ...props }: IconButtonProps & { toggle: Disclosure }) { + return ; +} + +type NotificationTypeTogglesPropTypes = Omit & { + showReplies: Disclosure; + showMentions: Disclosure; + showZaps: Disclosure; + showReposts: Disclosure; + showReactions: Disclosure; +}; + +export default function NotificationTypeToggles({ + showReplies, + showMentions, + showZaps, + showReposts, + showReactions, + ...props +}: NotificationTypeTogglesPropTypes) { + return ( + + } + aria-label="Toggle replies" + title="Toggle replies" + toggle={showReplies} + /> + } + aria-label="Toggle reposts" + title="Toggle reposts" + toggle={showMentions} + /> + } + aria-label="Toggle zaps" + title="Toggle zaps" + toggle={showZaps} + /> + } + aria-label="Toggle reposts" + title="Toggle reposts" + toggle={showReposts} + /> + } + aria-label="Toggle reactions" + title="Toggle reactions" + toggle={showReactions} + /> + + ); +} diff --git a/src/views/notifications/zaps.tsx b/src/views/notifications/zaps.tsx deleted file mode 100644 index 62ee2e632..000000000 --- a/src/views/notifications/zaps.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Kind } from "nostr-tools"; - -import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; -import useSubject from "../../hooks/use-subject"; -import { useNotificationTimeline } from "../../providers/notification-timeline"; -import NotificationItem from "./notification-item"; - -export default function ZapNotificationsTab() { - const timeline = useNotificationTimeline(); - const events = useSubject(timeline?.timeline).filter((e) => e.kind === Kind.Zap) ?? []; - - return ( - <> - {events.map((event) => ( - - ))} - - - ); -}