mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
Add threads notifications view
This commit is contained in:
parent
1f77a48494
commit
9fa2ae472b
5
.changeset/mighty-hairs-boil.md
Normal file
5
.changeset/mighty-hairs-boil.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add threads notifications view
|
@ -75,6 +75,7 @@ import DMFeedView from "./views/tools/dm-feed";
|
||||
import ContentDiscoveryView from "./views/tools/content-discovery";
|
||||
import ContentDiscoveryDVMView from "./views/tools/content-discovery/dvm";
|
||||
import LoginNostrConnectView from "./views/signin/nostr-connect";
|
||||
import ThreadsNotificationsView from "./views/notifications/threads";
|
||||
const UserTracksTab = lazy(() => import("./views/user/tracks"));
|
||||
|
||||
const ToolsHomeView = lazy(() => import("./views/tools"));
|
||||
@ -232,7 +233,13 @@ const router = createHashRouter([
|
||||
],
|
||||
},
|
||||
{ path: "r/:relay", element: <RelayView /> },
|
||||
{ path: "notifications", element: <NotificationsView /> },
|
||||
{
|
||||
path: "notifications",
|
||||
children: [
|
||||
{ path: "threads", element: <ThreadsNotificationsView /> },
|
||||
{ path: "", element: <NotificationsView /> },
|
||||
],
|
||||
},
|
||||
{ path: "search", element: <SearchView /> },
|
||||
{
|
||||
path: "dm",
|
||||
|
@ -98,14 +98,11 @@ export function filterTagsByContentRefs(content: string, tags: Tag[], referenced
|
||||
|
||||
export type EventReferences = ReturnType<typeof getReferences>;
|
||||
export function getReferences(event: NostrEvent | DraftNostrEvent) {
|
||||
const eTags = event.tags.filter(isETag);
|
||||
const pTags = event.tags.filter(isPTag);
|
||||
|
||||
const events = eTags.map((t) => t[1]);
|
||||
const contentTagRefs = getContentTagRefs(event.content, event.tags);
|
||||
|
||||
const replyTag = eTags.find((t) => t[3] === "reply");
|
||||
const rootTag = eTags.find((t) => t[3] === "root");
|
||||
const replyTag = event.tags.find((t) => t[3] === "reply");
|
||||
const rootTag = event.tags.find((t) => t[3] === "root");
|
||||
const mentionTags = event.tags.find((t) => t[3] === "mention");
|
||||
|
||||
let replyId = replyTag?.[1];
|
||||
let replyRelay = replyTag?.[2];
|
||||
@ -123,8 +120,8 @@ export function getReferences(event: NostrEvent | DraftNostrEvent) {
|
||||
|
||||
// legacy behavior
|
||||
// https://github.com/nostr-protocol/nips/blob/master/10.md#positional-e-tags-deprecated
|
||||
const legacyTags = eTags.filter((t, i) => {
|
||||
// ignore it if there is a third piece of data
|
||||
const legacyTags = event.tags.filter(isETag).filter((t, i) => {
|
||||
// ignore it if there is a type
|
||||
if (t[3]) return false;
|
||||
const tagIndex = event.tags.indexOf(t);
|
||||
if (contentTagRefs.includes(tagIndex)) return false;
|
||||
@ -140,11 +137,15 @@ export function getReferences(event: NostrEvent | DraftNostrEvent) {
|
||||
}
|
||||
|
||||
return {
|
||||
events,
|
||||
replyTag,
|
||||
rootTag,
|
||||
mentionTags,
|
||||
|
||||
rootId,
|
||||
rootRelay,
|
||||
replyId,
|
||||
replyRelay,
|
||||
|
||||
contentTagRefs,
|
||||
};
|
||||
}
|
||||
|
27
src/helpers/notification.ts
Normal file
27
src/helpers/notification.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import dayjs from "dayjs";
|
||||
import SuperMap from "../classes/super-map";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { getReferences } from "./nostr/events";
|
||||
|
||||
export function groupByDay(events: NostrEvent[]) {
|
||||
const grouped = new SuperMap<number, NostrEvent[]>(() => []);
|
||||
for (const event of events) {
|
||||
const day = dayjs.unix(event.created_at).startOf("day").unix();
|
||||
grouped.get(day).push(event);
|
||||
}
|
||||
|
||||
return Array.from(grouped.entries()).sort((a, b) => b[0] - a[0]);
|
||||
}
|
||||
|
||||
export function groupByRoot(events: NostrEvent[]) {
|
||||
const grouped = new SuperMap<string, NostrEvent[]>(() => []);
|
||||
for (const event of events) {
|
||||
const refs = getReferences(event);
|
||||
if (refs.rootId) grouped.get(refs.rootId).push(event);
|
||||
}
|
||||
for (const [_, groupedEvents] of grouped) {
|
||||
groupedEvents.sort((a, b) => b.created_at - a.created_at);
|
||||
}
|
||||
|
||||
return Array.from(grouped.entries()).sort((a, b) => b[1][0].created_at - a[1][0].created_at);
|
||||
}
|
@ -24,7 +24,7 @@ export function useNotificationTimeline() {
|
||||
|
||||
export default function NotificationTimelineProvider({ children }: PropsWithChildren) {
|
||||
const account = useCurrentAccount();
|
||||
const readRelays = useReadRelayUrls();
|
||||
const inbox = useReadRelayUrls();
|
||||
|
||||
const userMuteFilter = useClientSideMuteFilter();
|
||||
const eventFilter = useCallback(
|
||||
@ -37,7 +37,7 @@ export default function NotificationTimelineProvider({ children }: PropsWithChil
|
||||
|
||||
const timeline = useTimelineLoader(
|
||||
`${account?.pubkey ?? "anon"}-notification`,
|
||||
readRelays,
|
||||
inbox,
|
||||
account?.pubkey
|
||||
? {
|
||||
"#p": [account.pubkey],
|
||||
|
@ -125,7 +125,7 @@ export default function MessageBubble({
|
||||
{(plaintext) => (
|
||||
<MessageContent event={message} text={plaintext} display="inline">
|
||||
{!hasReactions && (
|
||||
<ButtonGroup size="xs" variant="ghost" float="right">
|
||||
<ButtonGroup size="xs" variant="ghost" float="right" ml="2">
|
||||
{actionPosition === "inline" && actions}
|
||||
<Timestamp timestamp={message.created_at} ml="2" />
|
||||
</ButtonGroup>
|
||||
|
38
src/views/notifications/components/day-group.tsx
Normal file
38
src/views/notifications/components/day-group.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { MutableRefObject, PropsWithChildren, forwardRef } from "react";
|
||||
import { Divider, Flex, Heading, useDisclosure } from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { ExpandableToggleButton } from "../notification-item";
|
||||
|
||||
const specialNames = {
|
||||
[dayjs().startOf("day").unix()]: "Today",
|
||||
[dayjs().subtract(1, "day").startOf("day").unix()]: "Yesterday",
|
||||
};
|
||||
|
||||
const DayGroup = forwardRef<HTMLDivElement, PropsWithChildren<{ day: number; hideRefOnClose?: boolean }>>(
|
||||
({ day, children, hideRefOnClose = false }, ref) => {
|
||||
const expanded = useDisclosure({ defaultIsOpen: true });
|
||||
const now = dayjs();
|
||||
const date = dayjs.unix(day);
|
||||
let title = specialNames[day] || date.fromNow();
|
||||
if (now.diff(date, "week") > 2) {
|
||||
title = date.format("L");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="4" alignItems="center" mt="4" ref={hideRefOnClose && !expanded ? undefined : ref}>
|
||||
<Divider w="10" flexShrink={0} />
|
||||
<Heading size="lg" whiteSpace="nowrap">
|
||||
{title}
|
||||
</Heading>
|
||||
<Divider />
|
||||
<ExpandableToggleButton toggle={expanded} aria-label="Toggle day" title="Toggle day" />
|
||||
</Flex>
|
||||
{expanded.isOpen && children}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default DayGroup;
|
@ -0,0 +1,19 @@
|
||||
import { Box, Flex } from "@chakra-ui/react";
|
||||
import { PropsWithChildren, ReactNode, forwardRef } from "react";
|
||||
|
||||
const NotificationIconEntry = forwardRef<HTMLDivElement, PropsWithChildren<{ icon: ReactNode }>>(
|
||||
({ children, icon }, ref) => {
|
||||
return (
|
||||
<Flex gap="2" ref={ref}>
|
||||
<Box px="2" pb="2">
|
||||
{icon}
|
||||
</Box>
|
||||
<Flex direction="column" w="full" gap="2" overflow="hidden">
|
||||
{children}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default NotificationIconEntry;
|
@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { Divider, Flex, Heading, useDisclosure } from "@chakra-ui/react";
|
||||
import { Button, ButtonGroup, Flex, useDisclosure } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||
@ -12,41 +13,22 @@ import { getEventUID, 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, { ExpandableToggleButton } from "./notification-item";
|
||||
import NotificationItem from "./notification-item";
|
||||
import NotificationTypeToggles from "./notification-type-toggles";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import dayjs from "dayjs";
|
||||
import SuperMap from "../../classes/super-map";
|
||||
|
||||
const specialNames = {
|
||||
[dayjs().startOf("day").unix()]: "Today",
|
||||
[dayjs().subtract(1, "day").startOf("day").unix()]: "Yesterday",
|
||||
};
|
||||
import { groupByDay } from "../../helpers/notification";
|
||||
import DayGroup from "./components/day-group";
|
||||
|
||||
function NotificationDay({ day, events }: { day: number; events: NostrEvent[] }) {
|
||||
const expanded = useDisclosure({ defaultIsOpen: true });
|
||||
const now = dayjs();
|
||||
const date = dayjs.unix(day);
|
||||
let title = specialNames[day] || date.fromNow();
|
||||
if (now.diff(date, "week") > 2) {
|
||||
title = date.format("L");
|
||||
}
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, expanded.isOpen ? undefined : getEventUID(events[events.length - 1]));
|
||||
useRegisterIntersectionEntity(ref, getEventUID(events[events.length - 1]));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="4" alignItems="center" mt="4" ref={ref}>
|
||||
<Divider w="10" flexShrink={0} />
|
||||
<Heading size="lg" whiteSpace="nowrap">
|
||||
{title}
|
||||
</Heading>
|
||||
<Divider />
|
||||
<ExpandableToggleButton toggle={expanded} aria-label="Toggle day" title="Toggle day" />
|
||||
</Flex>
|
||||
{expanded.isOpen && events.map((event) => <NotificationItem key={event.id} event={event} />)}
|
||||
</>
|
||||
<DayGroup day={day} ref={ref} hideRefOnClose>
|
||||
{events.map((event) => (
|
||||
<NotificationItem key={event.id} event={event} />
|
||||
))}
|
||||
</DayGroup>
|
||||
);
|
||||
}
|
||||
|
||||
@ -89,21 +71,12 @@ function NotificationsPage() {
|
||||
return true;
|
||||
});
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
const map = new SuperMap<number, NostrEvent[]>(() => []);
|
||||
for (const event of events) {
|
||||
const day = dayjs.unix(event.created_at).startOf("day").unix();
|
||||
map.get(day).push(event);
|
||||
}
|
||||
return map;
|
||||
}, [events]);
|
||||
|
||||
const sortedDays = Array.from(grouped.entries()).sort((a, b) => b[0] - a[0]);
|
||||
const sortedDays = useMemo(() => groupByDay(events), [events]);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout>
|
||||
<Flex gap="2">
|
||||
<Flex gap="2" wrap="wrap">
|
||||
<NotificationTypeToggles
|
||||
showReplies={showReplies}
|
||||
showMentions={showMentions}
|
||||
@ -111,7 +84,12 @@ function NotificationsPage() {
|
||||
showReactions={showReactions}
|
||||
showReposts={showReposts}
|
||||
/>
|
||||
<PeopleListSelection flexShrink={0} />
|
||||
<ButtonGroup>
|
||||
<PeopleListSelection flexShrink={0} />
|
||||
<Button as={RouterLink} to="/notifications/threads">
|
||||
Threads
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
|
||||
{sortedDays.map(([day, events]) => (
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { PropsWithChildren, ReactNode, forwardRef, memo, useMemo, useRef } from "react";
|
||||
import { AvatarGroup, Box, Flex, IconButton, IconButtonProps, Text, useDisclosure } from "@chakra-ui/react";
|
||||
import { ReactNode, forwardRef, memo, useMemo, useRef } from "react";
|
||||
import { AvatarGroup, Flex, IconButton, IconButtonProps, Text, useDisclosure } from "@chakra-ui/react";
|
||||
import { Kind, nip18, nip25 } from "nostr-tools";
|
||||
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
@ -17,12 +17,8 @@ import UserAvatarLink from "../../components/user-avatar-link";
|
||||
import { AtIcon, ChevronDownIcon, ChevronUpIcon, LightningIcon, ReplyIcon, RepostIcon } from "../../components/icons";
|
||||
import useSingleEvent from "../../hooks/use-single-event";
|
||||
import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents";
|
||||
import NotificationIconEntry from "./components/notification-icon-entry";
|
||||
|
||||
const IconBox = ({ children }: PropsWithChildren) => (
|
||||
<Box px="2" pb="2">
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
export const ExpandableToggleButton = ({
|
||||
toggle,
|
||||
...props
|
||||
@ -48,28 +44,16 @@ const NoteNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ ev
|
||||
else return null;
|
||||
});
|
||||
const ReplyNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => (
|
||||
<Flex gap="2" ref={ref}>
|
||||
<IconBox>
|
||||
<ReplyIcon boxSize={8} color="green.400" />
|
||||
</IconBox>
|
||||
<Flex direction="column" w="full" gap="2">
|
||||
<EmbedEvent event={event} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<NotificationIconEntry ref={ref} icon={<ReplyIcon boxSize={8} color="green.400" />}>
|
||||
<EmbedEvent event={event} />
|
||||
</NotificationIconEntry>
|
||||
));
|
||||
|
||||
const MentionNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
|
||||
return (
|
||||
<Flex gap="2" ref={ref}>
|
||||
<IconBox>
|
||||
<AtIcon boxSize={8} color="purple.400" />
|
||||
</IconBox>
|
||||
<Flex direction="column" w="full" gap="2">
|
||||
<EmbedEvent event={event} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
const MentionNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => (
|
||||
<NotificationIconEntry ref={ref} icon={<AtIcon boxSize={8} color="purple.400" />}>
|
||||
<EmbedEvent event={event} />
|
||||
</NotificationIconEntry>
|
||||
));
|
||||
|
||||
const RepostNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
|
||||
const account = useCurrentAccount()!;
|
||||
@ -79,20 +63,15 @@ const RepostNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({
|
||||
if (pointer?.author !== account.pubkey) return null;
|
||||
|
||||
return (
|
||||
<Flex gap="2" ref={ref}>
|
||||
<IconBox>
|
||||
<RepostIcon boxSize={8} color="blue.400" />
|
||||
</IconBox>
|
||||
<Flex direction="column" w="full" gap="2">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<AvatarGroup size="sm">
|
||||
<UserAvatarLink pubkey={event.pubkey} />
|
||||
</AvatarGroup>
|
||||
<ExpandableToggleButton aria-label="Toggle event" ml="auto" toggle={expanded} />
|
||||
</Flex>
|
||||
{expanded.isOpen && <EmbedEventPointer pointer={{ type: "nevent", data: pointer }} />}
|
||||
<NotificationIconEntry ref={ref} icon={<RepostIcon boxSize={8} color="blue.400" />}>
|
||||
<Flex gap="2" alignItems="center">
|
||||
<AvatarGroup size="sm">
|
||||
<UserAvatarLink pubkey={event.pubkey} />
|
||||
</AvatarGroup>
|
||||
<ExpandableToggleButton aria-label="Toggle event" ml="auto" toggle={expanded} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
{expanded.isOpen && <EmbedEventPointer pointer={{ type: "nevent", data: pointer }} />}
|
||||
</NotificationIconEntry>
|
||||
);
|
||||
});
|
||||
|
||||
@ -106,22 +85,17 @@ const ReactionNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>((
|
||||
if (reactedEvent?.kind === Kind.EncryptedDirectMessage) return null;
|
||||
|
||||
return (
|
||||
<Flex gap="2" ref={ref}>
|
||||
<IconBox>
|
||||
<Heart boxSize={8} color="red.400" />
|
||||
</IconBox>
|
||||
<Flex direction="column" w="full" gap="2">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<AvatarGroup size="sm">
|
||||
<UserAvatarLink pubkey={event.pubkey} />
|
||||
</AvatarGroup>
|
||||
<Text fontSize="xl">{event.content}</Text>
|
||||
<ExpandableToggleButton aria-label="Toggle event" ml="auto" toggle={expanded} />
|
||||
{/* <Timestamp timestamp={event.created_at} ml="auto" /> */}
|
||||
</Flex>
|
||||
{expanded.isOpen && <EmbedEventPointer pointer={{ type: "nevent", data: pointer }} />}
|
||||
<NotificationIconEntry ref={ref} icon={<Heart boxSize={8} color="red.400" />}>
|
||||
<Flex gap="2" alignItems="center">
|
||||
<AvatarGroup size="sm">
|
||||
<UserAvatarLink pubkey={event.pubkey} />
|
||||
</AvatarGroup>
|
||||
<Text fontSize="xl">{event.content}</Text>
|
||||
<ExpandableToggleButton aria-label="Toggle event" ml="auto" toggle={expanded} />
|
||||
{/* <Timestamp timestamp={event.created_at} ml="auto" /> */}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{expanded.isOpen && <EmbedEventPointer pointer={{ type: "nevent", data: pointer }} />}
|
||||
</NotificationIconEntry>
|
||||
);
|
||||
});
|
||||
|
||||
@ -158,22 +132,17 @@ const ZapNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ eve
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap="2" ref={ref}>
|
||||
<IconBox>
|
||||
<LightningIcon boxSize={8} color="yellow.400" />
|
||||
</IconBox>
|
||||
<Flex direction="column" w="full" gap="2">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<AvatarGroup size="sm">
|
||||
<UserAvatarLink pubkey={zap.request.pubkey} />
|
||||
</AvatarGroup>
|
||||
<Text>{readablizeSats(zap.payment.amount / 1000)} sats</Text>
|
||||
{zap.request.content && <Text>{zap.request.content}</Text>}
|
||||
{eventJSX !== null && <ExpandableToggleButton aria-label="Toggle event" ml="auto" toggle={expanded} />}
|
||||
</Flex>
|
||||
{expanded.isOpen && eventJSX}
|
||||
<NotificationIconEntry ref={ref} icon={<LightningIcon boxSize={8} color="yellow.400" />}>
|
||||
<Flex gap="2" alignItems="center">
|
||||
<AvatarGroup size="sm">
|
||||
<UserAvatarLink pubkey={zap.request.pubkey} />
|
||||
</AvatarGroup>
|
||||
<Text>{readablizeSats(zap.payment.amount / 1000)} sats</Text>
|
||||
{zap.request.content && <Text>{zap.request.content}</Text>}
|
||||
{eventJSX !== null && <ExpandableToggleButton aria-label="Toggle event" ml="auto" toggle={expanded} />}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{expanded.isOpen && eventJSX}
|
||||
</NotificationIconEntry>
|
||||
);
|
||||
});
|
||||
|
||||
|
140
src/views/notifications/threads.tsx
Normal file
140
src/views/notifications/threads.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import { MouseEventHandler, useCallback, useMemo, useRef } from "react";
|
||||
import { Kind } from "nostr-tools";
|
||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import { useNotificationTimeline } from "../../providers/notification-timeline";
|
||||
import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents";
|
||||
import { groupByRoot } from "../../helpers/notification";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import NotificationIconEntry from "./components/notification-icon-entry";
|
||||
import { ChevronLeftIcon, ReplyIcon } from "../../components/icons";
|
||||
import { AvatarGroup, Box, Button, ButtonGroup, Flex, LinkBox, Text, useDisclosure } from "@chakra-ui/react";
|
||||
import UserAvatarLink from "../../components/user-avatar-link";
|
||||
import useSingleEvent from "../../hooks/use-single-event";
|
||||
import UserLink from "../../components/user-link";
|
||||
import { CompactNoteContent } from "../../components/compact-note-content";
|
||||
import Timestamp from "../../components/timestamp";
|
||||
import HoverLinkOverlay from "../../components/hover-link-overlay";
|
||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import { useNavigateInDrawer } from "../../providers/drawer-sub-view-provider";
|
||||
|
||||
const THREAD_KINDS = [Kind.Text, TORRENT_COMMENT_KIND];
|
||||
|
||||
function ReplyEntry({ event }: { event: NostrEvent }) {
|
||||
const navigate = useNavigateInDrawer();
|
||||
const onClick = useCallback<MouseEventHandler>(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
navigate(`/n/${getSharableEventAddress(event)}`);
|
||||
},
|
||||
[navigate],
|
||||
);
|
||||
|
||||
return (
|
||||
<LinkBox>
|
||||
<Flex gap="2">
|
||||
<UserLink pubkey={event.pubkey} fontWeight="bold" />
|
||||
<Timestamp timestamp={event.created_at} />
|
||||
</Flex>
|
||||
<CompactNoteContent event={event} maxLength={100} />
|
||||
<HoverLinkOverlay as={RouterLink} to={`/n/${getSharableEventAddress(event)}`} onClick={onClick} />
|
||||
</LinkBox>
|
||||
);
|
||||
}
|
||||
|
||||
function ThreadGroup({ rootId, events }: { rootId: string; events: NostrEvent[] }) {
|
||||
const pubkeys = events.reduce<string[]>((arr, e) => {
|
||||
if (!arr.includes(e.pubkey)) arr.push(e.pubkey);
|
||||
return arr;
|
||||
}, []);
|
||||
const showAll = useDisclosure();
|
||||
|
||||
const rootEvent = useSingleEvent(rootId);
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(events[events.length - 1]));
|
||||
|
||||
return (
|
||||
<NotificationIconEntry icon={<ReplyIcon boxSize={8} />}>
|
||||
<AvatarGroup size="sm">
|
||||
{pubkeys.map((pubkey) => (
|
||||
<UserAvatarLink key={pubkey} pubkey={pubkey} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
<Box>
|
||||
<Text fontWeight="bold">
|
||||
{pubkeys.length > 1 ? pubkeys.length + " people" : pubkeys.length + " person"} replied in thread:
|
||||
</Text>
|
||||
{rootEvent && <CompactNoteContent event={rootEvent} maxLength={100} color="GrayText" />}
|
||||
</Box>
|
||||
{(events.length > 3 && !showAll.isOpen ? events.slice(0, 3) : events).map((event) => (
|
||||
<ReplyEntry key={event.id} event={event} />
|
||||
))}
|
||||
{!showAll.isOpen && events.length > 3 && (
|
||||
<ButtonGroup>
|
||||
<Button variant="link" py="2" onClick={showAll.onOpen} colorScheme="primary" fontWeight="bold">
|
||||
+{events.length - 3} more
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</NotificationIconEntry>
|
||||
);
|
||||
}
|
||||
|
||||
function ThreadsNotificationsPage() {
|
||||
const navigate = useNavigate();
|
||||
const account = useCurrentAccount();
|
||||
const { people } = usePeopleListContext();
|
||||
const peoplePubkeys = useMemo(() => people?.map((p) => p.pubkey), [people]);
|
||||
|
||||
const timeline = useNotificationTimeline();
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
const events = useSubject(timeline?.timeline);
|
||||
|
||||
const filteredEvents = useMemo(
|
||||
() =>
|
||||
events.filter((e) => {
|
||||
if (!THREAD_KINDS.includes(e.kind)) return false;
|
||||
if (peoplePubkeys && !peoplePubkeys.includes(e.pubkey)) return false;
|
||||
return true;
|
||||
}),
|
||||
[events],
|
||||
);
|
||||
const threads = useMemo(() => groupByRoot(filteredEvents), [filteredEvents]);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout>
|
||||
<Flex gap="2">
|
||||
<Button leftIcon={<ChevronLeftIcon />} onClick={() => navigate(-1)}>
|
||||
Back
|
||||
</Button>
|
||||
<PeopleListSelection />
|
||||
</Flex>
|
||||
{threads.map((thread) => (
|
||||
<ThreadGroup key={thread[0]} rootId={thread[0]} events={thread[1]} />
|
||||
))}
|
||||
</VerticalPageLayout>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ThreadsNotificationsView() {
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<PeopleListProvider initList="global">
|
||||
<ThreadsNotificationsPage />
|
||||
</PeopleListProvider>
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user