mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-22 15:19:47 +02:00
notifications v6
This commit is contained in:
@@ -7,7 +7,7 @@ export default function Timestamp({ timestamp, ...props }: { timestamp: number }
|
|||||||
|
|
||||||
let display = date.format("L");
|
let display = date.format("L");
|
||||||
|
|
||||||
if (now.diff(date, "week") <= 2) {
|
if (now.diff(date, "week") <= 6) {
|
||||||
if (now.diff(date, "d") >= 1) {
|
if (now.diff(date, "d") >= 1) {
|
||||||
display = Math.round(now.diff(date, "d") * 10) / 10 + `d`;
|
display = Math.round(now.diff(date, "d") * 10) / 10 + `d`;
|
||||||
} else if (now.diff(date, "h") >= 1) {
|
} else if (now.diff(date, "h") >= 1) {
|
||||||
|
13
src/hooks/use-read-status.ts
Normal file
13
src/hooks/use-read-status.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import readStatusService from "../services/read-status";
|
||||||
|
import useSubject from "./use-subject";
|
||||||
|
|
||||||
|
export default function useReadStatus(key: string, ttl?: number) {
|
||||||
|
const subject = useMemo(() => readStatusService.getStatus(key, ttl), [key]);
|
||||||
|
|
||||||
|
const setRead = useCallback((read = true) => readStatusService.setRead(key, read, ttl), [key, ttl]);
|
||||||
|
|
||||||
|
const read = useSubject(subject);
|
||||||
|
|
||||||
|
return [read, setRead] as const;
|
||||||
|
}
|
@@ -1,15 +1,15 @@
|
|||||||
import { openDB, deleteDB, IDBPDatabase, IDBPTransaction } from "idb";
|
import { openDB, deleteDB, IDBPDatabase, IDBPTransaction } from "idb";
|
||||||
import { clearDB, deleteDB as nostrIDBDelete } from "nostr-idb";
|
import { clearDB, deleteDB as nostrIDBDelete } from "nostr-idb";
|
||||||
|
|
||||||
import { SchemaV1, SchemaV2, SchemaV3, SchemaV4, SchemaV5, SchemaV6, SchemaV7, SchemaV8 } from "./schema";
|
import { SchemaV1, SchemaV2, SchemaV3, SchemaV4, SchemaV5, SchemaV6, SchemaV7, SchemaV8, SchemaV9 } from "./schema";
|
||||||
import { logger } from "../../helpers/debug";
|
import { logger } from "../../helpers/debug";
|
||||||
import { localDatabase } from "../local-relay";
|
import { localDatabase } from "../local-relay";
|
||||||
|
|
||||||
const log = logger.extend("Database");
|
const log = logger.extend("Database");
|
||||||
|
|
||||||
const dbName = "storage";
|
const dbName = "storage";
|
||||||
const version = 8;
|
const version = 9;
|
||||||
const db = await openDB<SchemaV8>(dbName, version, {
|
const db = await openDB<SchemaV9>(dbName, version, {
|
||||||
upgrade(db, oldVersion, newVersion, transaction, event) {
|
upgrade(db, oldVersion, newVersion, transaction, event) {
|
||||||
if (oldVersion < 1) {
|
if (oldVersion < 1) {
|
||||||
const v0 = db as unknown as IDBPDatabase<SchemaV1>;
|
const v0 = db as unknown as IDBPDatabase<SchemaV1>;
|
||||||
@@ -171,6 +171,13 @@ const db = await openDB<SchemaV8>(dbName, version, {
|
|||||||
const v7 = db as unknown as IDBPDatabase<SchemaV7>;
|
const v7 = db as unknown as IDBPDatabase<SchemaV7>;
|
||||||
v7.deleteObjectStore("replaceableEvents");
|
v7.deleteObjectStore("replaceableEvents");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < 9) {
|
||||||
|
const v9 = db as unknown as IDBPDatabase<SchemaV9>;
|
||||||
|
|
||||||
|
const readStore = v9.createObjectStore("read", { keyPath: "key" });
|
||||||
|
readStore.createIndex("ttl", "ttl");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -141,3 +141,15 @@ export interface SchemaV7 extends Omit<SchemaV6, "accounts"> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SchemaV8 extends Omit<SchemaV7, "replaceableEvents"> {}
|
export interface SchemaV8 extends Omit<SchemaV7, "replaceableEvents"> {}
|
||||||
|
|
||||||
|
export interface SchemaV9 extends SchemaV8 {
|
||||||
|
read: {
|
||||||
|
key: string;
|
||||||
|
value: {
|
||||||
|
key: string;
|
||||||
|
ttl: number;
|
||||||
|
read: boolean;
|
||||||
|
};
|
||||||
|
indexes: { ttl: number };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
106
src/services/read-status.ts
Normal file
106
src/services/read-status.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import _throttle from "lodash.throttle";
|
||||||
|
|
||||||
|
import Subject from "../classes/subject";
|
||||||
|
import SuperMap from "../classes/super-map";
|
||||||
|
import db from "./db";
|
||||||
|
import { logger } from "../helpers/debug";
|
||||||
|
|
||||||
|
class ReadStatusService {
|
||||||
|
log = logger.extend("ReadStatusService");
|
||||||
|
status = new SuperMap<string, Subject<boolean>>(() => new Subject());
|
||||||
|
ttl = new Map<string, number>();
|
||||||
|
|
||||||
|
private setTTL(key: string, ttl: number) {
|
||||||
|
const current = this.ttl.get(key);
|
||||||
|
if (!current || ttl > current) {
|
||||||
|
this.ttl.set(key, ttl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus(key: string, ttl?: number) {
|
||||||
|
const subject = this.status.get(key);
|
||||||
|
|
||||||
|
if (ttl) this.setTTL(key, ttl);
|
||||||
|
else this.setTTL(key, dayjs().add(1, "day").unix());
|
||||||
|
|
||||||
|
if (subject.value === undefined && !this.queue.has(key)) {
|
||||||
|
this.queue.add(key);
|
||||||
|
this.throttleRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRead(key: string, read = true, ttl?: number) {
|
||||||
|
if (ttl) this.setTTL(key, ttl);
|
||||||
|
else this.setTTL(key, dayjs().add(1, "day").unix());
|
||||||
|
|
||||||
|
this.status.get(key).next(read);
|
||||||
|
this.throttleWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
queue = new Set<string>();
|
||||||
|
private throttleRead = _throttle(this.read.bind(this), 1000);
|
||||||
|
async read() {
|
||||||
|
if (this.queue.size === 0) return;
|
||||||
|
|
||||||
|
const trans = db.transaction("read");
|
||||||
|
|
||||||
|
this.log(`Loading ${this.queue.size} from database`);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(this.queue).map(async (key) => {
|
||||||
|
const subject = this.status.get(key);
|
||||||
|
const status = await trans.store.get(key);
|
||||||
|
if (status) {
|
||||||
|
subject.next(status.read);
|
||||||
|
if (status.ttl) this.setTTL(key, status.ttl);
|
||||||
|
} else subject.next(false);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.queue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
throttleWrite = _throttle(this.write.bind(this), 1000);
|
||||||
|
async write() {
|
||||||
|
const trans = db.transaction("read", "readwrite");
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
const defaultTTL = dayjs().add(1, "day").unix();
|
||||||
|
for (const [key, subject] of this.status) {
|
||||||
|
if (subject.value !== undefined) {
|
||||||
|
trans.store.add({ key, read: subject.value, ttl: this.ttl.get(key) ?? defaultTTL });
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trans.done;
|
||||||
|
|
||||||
|
this.log(`Wrote ${count} to database`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async prune() {
|
||||||
|
const expired = await db.getAllKeysFromIndex("read", "ttl", IDBKeyRange.lowerBound(dayjs().unix(), true));
|
||||||
|
|
||||||
|
if (expired.length === 0) return;
|
||||||
|
|
||||||
|
const tx = db.transaction("read", "readwrite");
|
||||||
|
await Promise.all(expired.map((key) => tx.store.delete(key)));
|
||||||
|
await tx.done;
|
||||||
|
|
||||||
|
this.log(`Removed ${expired.length} expired entries`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const readStatusService = new ReadStatusService();
|
||||||
|
|
||||||
|
setInterval(readStatusService.write.bind(readStatusService), 10_000);
|
||||||
|
setInterval(readStatusService.prune.bind(readStatusService), 30_000);
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// @ts-expect-error
|
||||||
|
window.readStatusService = readStatusService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default readStatusService;
|
@@ -6,6 +6,8 @@ import { useNotifications } from "../../../providers/global/notifications-provid
|
|||||||
import useSubject from "../../../hooks/use-subject";
|
import useSubject from "../../../hooks/use-subject";
|
||||||
import { NotificationType, typeSymbol } from "../../../classes/notifications";
|
import { NotificationType, typeSymbol } from "../../../classes/notifications";
|
||||||
import NotificationItem from "../../notifications/components/notification-item";
|
import NotificationItem from "../../notifications/components/notification-item";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
|
||||||
export default function NotificationsCard({ ...props }: Omit<CardProps, "children">) {
|
export default function NotificationsCard({ ...props }: Omit<CardProps, "children">) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -21,6 +23,13 @@ export default function NotificationsCard({ ...props }: Omit<CardProps, "childre
|
|||||||
|
|
||||||
const limit = events.length > 20 ? events.slice(0, 20) : events;
|
const limit = events.length > 20 ? events.slice(0, 20) : events;
|
||||||
|
|
||||||
|
const handleClick = useCallback(
|
||||||
|
(event: NostrEvent) => {
|
||||||
|
navigate("/notifications", { state: { focused: event.id } });
|
||||||
|
},
|
||||||
|
[navigate],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="outline" {...props}>
|
<Card variant="outline" {...props}>
|
||||||
<CardHeader display="flex" justifyContent="space-between" alignItems="center" pb="2">
|
<CardHeader display="flex" justifyContent="space-between" alignItems="center" pb="2">
|
||||||
@@ -33,7 +42,7 @@ export default function NotificationsCard({ ...props }: Omit<CardProps, "childre
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody overflowX="hidden" overflowY="auto" pt="4" display="flex" gap="2" flexDirection="column" maxH="50vh">
|
<CardBody overflowX="hidden" overflowY="auto" pt="4" display="flex" gap="2" flexDirection="column" maxH="50vh">
|
||||||
{limit.map((event) => (
|
{limit.map((event) => (
|
||||||
<NotificationItem event={event} key={event.id} />
|
<NotificationItem event={event} key={event.id} onClick={handleClick} />
|
||||||
))}
|
))}
|
||||||
<Button as={RouterLink} to="/notifications" flexShrink={0} variant="link" size="lg" py="6">
|
<Button as={RouterLink} to="/notifications" flexShrink={0} variant="link" size="lg" py="6">
|
||||||
View More
|
View More
|
||||||
|
@@ -1,19 +1,86 @@
|
|||||||
import { Box, Flex } from "@chakra-ui/react";
|
import { Box, Flex, Spacer, Text } from "@chakra-ui/react";
|
||||||
import { PropsWithChildren, ReactNode, forwardRef } from "react";
|
import { PropsWithChildren, ReactNode, forwardRef, memo, useCallback, useContext, useEffect } from "react";
|
||||||
|
import UserAvatar from "../../../components/user/user-avatar";
|
||||||
|
import Timestamp from "../../../components/timestamp";
|
||||||
|
import UserName from "../../../components/user/user-name";
|
||||||
|
import { CheckIcon } from "../../../components/icons";
|
||||||
|
import FocusedContext from "../focused-context";
|
||||||
|
|
||||||
const NotificationIconEntry = forwardRef<HTMLDivElement, PropsWithChildren<{ icon: ReactNode }>>(
|
// const ONE_MONTH = 60 * 60 * 24 * 30;
|
||||||
({ children, icon }, ref) => {
|
|
||||||
return (
|
type NotificationIconEntryProps = PropsWithChildren<{
|
||||||
<Flex gap="2" ref={ref}>
|
icon: ReactNode;
|
||||||
<Box px="2" pb="2">
|
pubkey: string;
|
||||||
{icon}
|
timestamp: number;
|
||||||
</Box>
|
summary: ReactNode;
|
||||||
<Flex direction="column" w="full" gap="2" overflow="hidden">
|
id: string;
|
||||||
{children}
|
onClick?: () => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const NotificationIconEntry = memo(
|
||||||
|
forwardRef<HTMLDivElement, NotificationIconEntryProps>(
|
||||||
|
({ children, icon, pubkey, timestamp, summary, id, onClick }, ref) => {
|
||||||
|
const { id: focused, focus } = useContext(FocusedContext);
|
||||||
|
// const [read, setRead] = useReadStatus(id, ONE_MONTH);
|
||||||
|
|
||||||
|
const expanded = focused === id;
|
||||||
|
|
||||||
|
const focusSelf = useCallback(() => focus(id), [id, focus]);
|
||||||
|
|
||||||
|
// scroll element to stop when opened
|
||||||
|
useEffect(() => {
|
||||||
|
if (expanded) {
|
||||||
|
// @ts-expect-error
|
||||||
|
ref.current?.scrollIntoView();
|
||||||
|
}
|
||||||
|
}, [expanded]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (!read && expanded) setRead(true);
|
||||||
|
// }, [read, expanded]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" bg={expanded ? "whiteAlpha.100" : undefined} rounded="md">
|
||||||
|
<Flex
|
||||||
|
gap="2"
|
||||||
|
alignItems="center"
|
||||||
|
ref={ref}
|
||||||
|
cursor="pointer"
|
||||||
|
p="2"
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={onClick ? undefined : focusSelf}
|
||||||
|
onClick={onClick}
|
||||||
|
userSelect="none"
|
||||||
|
bg={expanded ? "whiteAlpha.100" : undefined}
|
||||||
|
>
|
||||||
|
<Box>{icon}</Box>
|
||||||
|
<UserAvatar pubkey={pubkey} size="sm" />
|
||||||
|
<UserName pubkey={pubkey} hideBelow="md" />
|
||||||
|
<Text isTruncated>{summary}</Text>
|
||||||
|
<Spacer />
|
||||||
|
{/* {read && <CheckIcon boxSize={5} color="green.500" />} */}
|
||||||
|
<Timestamp timestamp={timestamp} />
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{expanded && (
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
w="full"
|
||||||
|
gap="2"
|
||||||
|
overflow="hidden"
|
||||||
|
minH={{ base: "calc(100vh - 10rem)", lg: "xs" }}
|
||||||
|
p="2"
|
||||||
|
maxH="calc(100vh - 10rem)"
|
||||||
|
overflowY="auto"
|
||||||
|
overflowX="hidden"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export default NotificationIconEntry;
|
export default NotificationIconEntry;
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import { ReactNode, forwardRef, memo, useMemo } from "react";
|
import { ReactNode, forwardRef, memo, useCallback, useMemo } from "react";
|
||||||
import { AvatarGroup, ButtonGroup, Flex, IconButton, IconButtonProps, Text, useDisclosure } from "@chakra-ui/react";
|
import { AvatarGroup, ButtonGroup, Flex, IconButton, IconButtonProps, Text } from "@chakra-ui/react";
|
||||||
import { kinds, nip18, nip25 } from "nostr-tools";
|
import { kinds, nip18, nip25 } from "nostr-tools";
|
||||||
|
import { DecodeResult } from "nostr-tools/nip19";
|
||||||
|
|
||||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||||
import { NostrEvent, isATag, isETag } from "../../../types/nostr-event";
|
import { NostrEvent, isATag, isETag } from "../../../types/nostr-event";
|
||||||
import { getParsedZap } from "../../../helpers/nostr/zaps";
|
import { getParsedZap } from "../../../helpers/nostr/zaps";
|
||||||
import { readablizeSats } from "../../../helpers/bolt11";
|
import { readablizeSats } from "../../../helpers/bolt11";
|
||||||
import { parseCoordinate } from "../../../helpers/nostr/event";
|
import { getThreadReferences, parseCoordinate } from "../../../helpers/nostr/event";
|
||||||
import { EmbedEvent, EmbedEventPointer } from "../../../components/embed-event";
|
import { EmbedEventPointer } from "../../../components/embed-event";
|
||||||
import EmbeddedUnknown from "../../../components/embed-event/event-types/embedded-unknown";
|
import EmbeddedUnknown from "../../../components/embed-event/event-types/embedded-unknown";
|
||||||
import { ErrorBoundary } from "../../../components/error-boundary";
|
import { ErrorBoundary } from "../../../components/error-boundary";
|
||||||
import { TrustProvider } from "../../../providers/local/trust-provider";
|
import { TrustProvider } from "../../../providers/local/trust-provider";
|
||||||
@@ -26,6 +27,12 @@ import NotificationIconEntry from "./notification-icon-entry";
|
|||||||
import { CategorizedEvent, NotificationType, typeSymbol } from "../../../classes/notifications";
|
import { CategorizedEvent, NotificationType, typeSymbol } from "../../../classes/notifications";
|
||||||
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
|
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
|
||||||
import ZapReceiptMenu from "../../../components/zap/zap-receipt-menu";
|
import ZapReceiptMenu from "../../../components/zap/zap-receipt-menu";
|
||||||
|
import ReactionIcon from "../../../components/event-reactions/reaction-icon";
|
||||||
|
import { TimelineNote } from "../../../components/note/timeline-note";
|
||||||
|
import UserAvatar from "../../../components/user/user-avatar";
|
||||||
|
import UserName from "../../../components/user/user-name";
|
||||||
|
import { truncateId } from "../../../helpers/string";
|
||||||
|
import TextNoteContents from "../../../components/note/timeline-note/text-note-contents";
|
||||||
|
|
||||||
export const ExpandableToggleButton = ({
|
export const ExpandableToggleButton = ({
|
||||||
toggle,
|
toggle,
|
||||||
@@ -39,125 +46,191 @@ export const ExpandableToggleButton = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ReplyNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => (
|
const ReplyNotification = forwardRef<HTMLDivElement, { event: NostrEvent; onClick?: () => void }>(
|
||||||
<NotificationIconEntry ref={ref} icon={<ReplyIcon boxSize={8} color="green.400" />}>
|
({ event, onClick }, ref) => {
|
||||||
<EmbedEvent event={event} />
|
const refs = getThreadReferences(event);
|
||||||
</NotificationIconEntry>
|
|
||||||
));
|
|
||||||
|
|
||||||
const MentionNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => (
|
const pointer = useMemo<DecodeResult | undefined>(() => {
|
||||||
<NotificationIconEntry ref={ref} icon={<AtIcon boxSize={8} color="purple.400" />}>
|
if (refs.reply?.a) return { type: "naddr", data: refs.reply.a };
|
||||||
<EmbedEvent event={event} />
|
if (refs.reply?.e) return { type: "nevent", data: refs.reply.e };
|
||||||
</NotificationIconEntry>
|
}, [refs.reply?.e, refs.reply?.a]);
|
||||||
));
|
|
||||||
|
|
||||||
const RepostNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
|
return (
|
||||||
const pointer = nip18.getRepostedEventPointer(event);
|
<NotificationIconEntry
|
||||||
const expanded = useDisclosure({ defaultIsOpen: true });
|
ref={ref}
|
||||||
if (!pointer) return null;
|
icon={<ReplyIcon boxSize={8} color="green.400" />}
|
||||||
|
id={event.id}
|
||||||
return (
|
pubkey={event.pubkey}
|
||||||
<NotificationIconEntry ref={ref} icon={<RepostIcon boxSize={8} color="blue.400" />}>
|
timestamp={event.created_at}
|
||||||
<Flex gap="2" alignItems="center">
|
summary={event.content}
|
||||||
<AvatarGroup size="sm">
|
onClick={onClick}
|
||||||
<UserAvatarLink pubkey={event.pubkey} />
|
>
|
||||||
</AvatarGroup>
|
{pointer && <EmbedEventPointer pointer={pointer} />}
|
||||||
<ExpandableToggleButton aria-label="Toggle event" ml="auto" toggle={expanded} />
|
<TimelineNote event={event} showReplyLine={false} />
|
||||||
</Flex>
|
</NotificationIconEntry>
|
||||||
{expanded.isOpen && <EmbedEventPointer pointer={{ type: "nevent", data: pointer }} />}
|
|
||||||
</NotificationIconEntry>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ReactionNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ 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;
|
|
||||||
|
|
||||||
const reactedEvent = useSingleEvent(pointer.id, pointer.relays);
|
|
||||||
if (reactedEvent?.kind === kinds.EncryptedDirectMessage) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NotificationIconEntry ref={ref} icon={<Heart boxSize={8} color="red.400" />}>
|
|
||||||
<Flex gap="2" alignItems="center" pl="2">
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ZapNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
|
|
||||||
const zap = useMemo(() => getParsedZap(event), [event]);
|
|
||||||
|
|
||||||
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;
|
|
||||||
const expanded = useDisclosure({ defaultIsOpen: true });
|
|
||||||
|
|
||||||
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 (
|
const MentionNotification = forwardRef<HTMLDivElement, { event: NostrEvent; onClick?: () => void }>(
|
||||||
<NotificationIconEntry ref={ref} icon={<LightningIcon boxSize={8} color="yellow.400" />}>
|
({ event, onClick }, ref) => (
|
||||||
<Flex gap="2" alignItems="center" pl="2">
|
<NotificationIconEntry
|
||||||
<AvatarGroup size="sm">
|
ref={ref}
|
||||||
<UserAvatarLink pubkey={zap.request.pubkey} />
|
icon={<AtIcon boxSize={8} color="purple.400" />}
|
||||||
</AvatarGroup>
|
id={event.id}
|
||||||
<Text>{readablizeSats(zap.payment.amount / 1000)} sats</Text>
|
pubkey={event.pubkey}
|
||||||
{zap.request.content && <Text>{zap.request.content}</Text>}
|
timestamp={event.created_at}
|
||||||
<ButtonGroup size="sm" variant="ghost" ml="auto">
|
summary={event.content}
|
||||||
{eventJSX !== null && <ExpandableToggleButton aria-label="Toggle event" toggle={expanded} />}
|
onClick={onClick}
|
||||||
<ZapReceiptMenu zap={zap.event} aria-label="More Options" />
|
>
|
||||||
</ButtonGroup>
|
<TimelineNote event={event} showReplyButton />
|
||||||
</Flex>
|
|
||||||
{expanded.isOpen && eventJSX}
|
|
||||||
</NotificationIconEntry>
|
</NotificationIconEntry>
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
|
|
||||||
const NotificationItem = ({ event }: { event: CategorizedEvent }) => {
|
const RepostNotification = forwardRef<HTMLDivElement, { event: NostrEvent; onClick?: () => void }>(
|
||||||
|
({ event, onClick }, ref) => {
|
||||||
|
const pointer = nip18.getRepostedEventPointer(event);
|
||||||
|
if (!pointer) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationIconEntry
|
||||||
|
ref={ref}
|
||||||
|
icon={<RepostIcon boxSize={8} color="blue.400" />}
|
||||||
|
id={event.id}
|
||||||
|
pubkey={event.pubkey}
|
||||||
|
timestamp={event.created_at}
|
||||||
|
summary={<>Reposted {truncateId(pointer.id)}</>}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
<UserAvatar size="xs" pubkey={event.pubkey} /> <UserName pubkey={event.pubkey} /> reposted:
|
||||||
|
</Text>
|
||||||
|
<EmbedEventPointer pointer={{ type: "nevent", data: pointer }} />
|
||||||
|
</NotificationIconEntry>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const ReactionNotification = forwardRef<HTMLDivElement, { event: NostrEvent; onClick?: () => void }>(
|
||||||
|
({ event, onClick }, ref) => {
|
||||||
|
const account = useCurrentAccount();
|
||||||
|
const pointer = nip25.getReactedEventPointer(event);
|
||||||
|
if (!pointer || (account?.pubkey && pointer.author !== account.pubkey)) return null;
|
||||||
|
|
||||||
|
const reactedEvent = useSingleEvent(pointer.id, pointer.relays);
|
||||||
|
if (reactedEvent?.kind === kinds.EncryptedDirectMessage) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationIconEntry
|
||||||
|
ref={ref}
|
||||||
|
icon={<Heart boxSize={8} color="red.400" />}
|
||||||
|
id={event.id}
|
||||||
|
pubkey={event.pubkey}
|
||||||
|
timestamp={event.created_at}
|
||||||
|
summary={
|
||||||
|
<>
|
||||||
|
<ReactionIcon emoji={event.content} />
|
||||||
|
{reactedEvent?.content}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Flex gap="2" alignItems="center" pl="2">
|
||||||
|
<AvatarGroup size="sm">
|
||||||
|
<UserAvatarLink pubkey={event.pubkey} />
|
||||||
|
</AvatarGroup>
|
||||||
|
<Text>
|
||||||
|
reacted with <ReactionIcon emoji={event.content} />
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<EmbedEventPointer pointer={{ type: "nevent", data: pointer }} />
|
||||||
|
</NotificationIconEntry>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const ZapNotification = forwardRef<HTMLDivElement, { event: NostrEvent; onClick?: () => void }>(
|
||||||
|
({ event, onClick }, ref) => {
|
||||||
|
const zap = useMemo(() => getParsedZap(event), [event]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<NotificationIconEntry
|
||||||
|
ref={ref}
|
||||||
|
icon={<LightningIcon boxSize={8} color="yellow.400" />}
|
||||||
|
id={event.id}
|
||||||
|
pubkey={zap.request.pubkey}
|
||||||
|
timestamp={zap.request.created_at}
|
||||||
|
summary={
|
||||||
|
<>
|
||||||
|
{readablizeSats(zap.payment.amount / 1000)} {zap.request.content}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Flex gap="2" alignItems="center" pl="2">
|
||||||
|
<AvatarGroup size="sm">
|
||||||
|
<UserAvatarLink pubkey={zap.request.pubkey} />
|
||||||
|
</AvatarGroup>
|
||||||
|
<Text>zapped {readablizeSats(zap.payment.amount / 1000)} sats</Text>
|
||||||
|
<ButtonGroup size="sm" variant="ghost" ml="auto">
|
||||||
|
<ZapReceiptMenu zap={zap.event} aria-label="More Options" />
|
||||||
|
</ButtonGroup>
|
||||||
|
</Flex>
|
||||||
|
<TextNoteContents event={zap.request} />
|
||||||
|
{eventJSX}
|
||||||
|
</NotificationIconEntry>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const NotificationItem = ({ event, onClick }: { event: CategorizedEvent; onClick?: (event: NostrEvent) => void }) => {
|
||||||
const ref = useEventIntersectionRef(event);
|
const ref = useEventIntersectionRef(event);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (onClick) onClick(event);
|
||||||
|
}, [onClick, event]);
|
||||||
|
|
||||||
let content: ReactNode | null = null;
|
let content: ReactNode | null = null;
|
||||||
switch (event[typeSymbol]) {
|
switch (event[typeSymbol]) {
|
||||||
case NotificationType.Reply:
|
case NotificationType.Reply:
|
||||||
content = <ReplyNotification event={event} ref={ref} />;
|
content = <ReplyNotification event={event} onClick={onClick && handleClick} ref={ref} />;
|
||||||
break;
|
break;
|
||||||
case NotificationType.Mention:
|
case NotificationType.Mention:
|
||||||
content = <MentionNotification event={event} ref={ref} />;
|
content = <MentionNotification event={event} onClick={onClick && handleClick} ref={ref} />;
|
||||||
break;
|
break;
|
||||||
case NotificationType.Reaction:
|
case NotificationType.Reaction:
|
||||||
content = <ReactionNotification event={event} ref={ref} />;
|
content = <ReactionNotification event={event} onClick={onClick && handleClick} ref={ref} />;
|
||||||
break;
|
break;
|
||||||
case NotificationType.Repost:
|
case NotificationType.Repost:
|
||||||
content = <RepostNotification event={event} ref={ref} />;
|
content = <RepostNotification event={event} onClick={onClick && handleClick} ref={ref} />;
|
||||||
break;
|
break;
|
||||||
case NotificationType.Zap:
|
case NotificationType.Zap:
|
||||||
content = <ZapNotification event={event} ref={ref} />;
|
content = <ZapNotification event={event} onClick={onClick && handleClick} ref={ref} />;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
content = <EmbeddedUnknown event={event} />;
|
content = <EmbeddedUnknown event={event} />;
|
||||||
|
5
src/views/notifications/focused-context.ts
Normal file
5
src/views/notifications/focused-context.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
const FocusedContext = createContext({ id: "", focus: (id: string) => {} });
|
||||||
|
|
||||||
|
export default FocusedContext;
|
@@ -1,7 +1,8 @@
|
|||||||
import { memo, useMemo } from "react";
|
import { memo, ReactNode, useContext, useMemo } from "react";
|
||||||
import { Button, ButtonGroup, Flex, IconButton, Input } from "@chakra-ui/react";
|
import { Button, ButtonGroup, Divider, Flex, Text } from "@chakra-ui/react";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { useKeyPressEvent } from "react-use";
|
||||||
|
|
||||||
import RequireCurrentAccount from "../../providers/route/require-current-account";
|
import RequireCurrentAccount from "../../providers/route/require-current-account";
|
||||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||||
@@ -13,23 +14,24 @@ import PeopleListSelection from "../../components/people-list-selection/people-l
|
|||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import NotificationItem from "./components/notification-item";
|
import NotificationItem from "./components/notification-item";
|
||||||
import NotificationTypeToggles from "./notification-type-toggles";
|
import NotificationTypeToggles from "./notification-type-toggles";
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "../../components/icons";
|
|
||||||
import useRouteSearchValue from "../../hooks/use-route-search-value";
|
|
||||||
import useLocalStorageDisclosure from "../../hooks/use-localstorage-disclosure";
|
import useLocalStorageDisclosure from "../../hooks/use-localstorage-disclosure";
|
||||||
import { NotificationType, typeSymbol } from "../../classes/notifications";
|
import { NotificationType, typeSymbol } from "../../classes/notifications";
|
||||||
|
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
|
||||||
|
import FocusedContext from "./focused-context";
|
||||||
|
import useRouteStateValue from "../../hooks/use-route-state-value";
|
||||||
|
|
||||||
const DATE_FORMAT = "YYYY-MM-DD";
|
// const DATE_FORMAT = "YYYY-MM-DD";
|
||||||
|
|
||||||
const NotificationsTimeline = memo(
|
const NotificationsTimeline = memo(
|
||||||
({
|
({
|
||||||
day,
|
// day,
|
||||||
showReplies,
|
showReplies,
|
||||||
showMentions,
|
showMentions,
|
||||||
showZaps,
|
showZaps,
|
||||||
showReposts,
|
showReposts,
|
||||||
showReactions,
|
showReactions,
|
||||||
}: {
|
}: {
|
||||||
day: string;
|
// day: string;
|
||||||
showReplies: boolean;
|
showReplies: boolean;
|
||||||
showMentions: boolean;
|
showMentions: boolean;
|
||||||
showZaps: boolean;
|
showZaps: boolean;
|
||||||
@@ -39,15 +41,15 @@ const NotificationsTimeline = memo(
|
|||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const { people } = usePeopleListContext();
|
const { people } = usePeopleListContext();
|
||||||
const peoplePubkeys = useMemo(() => people?.map((p) => p.pubkey), [people]);
|
const peoplePubkeys = useMemo(() => people?.map((p) => p.pubkey), [people]);
|
||||||
const minTimestamp = dayjs(day, DATE_FORMAT).startOf("day").unix();
|
// const minTimestamp = dayjs(day, DATE_FORMAT).startOf("day").unix();
|
||||||
const maxTimestamp = dayjs(day, DATE_FORMAT).endOf("day").unix();
|
// const maxTimestamp = dayjs(day, DATE_FORMAT).endOf("day").unix();
|
||||||
|
|
||||||
const events = useSubject(notifications?.timeline) ?? [];
|
const events = useSubject(notifications?.timeline) ?? [];
|
||||||
|
|
||||||
const filteredEvents = useMemo(
|
const filteredEvents = useMemo(
|
||||||
() =>
|
() =>
|
||||||
events.filter((e) => {
|
events.filter((e) => {
|
||||||
if (e.created_at < minTimestamp || e.created_at > maxTimestamp) return false;
|
// if (e.created_at < minTimestamp || e.created_at > maxTimestamp) return false;
|
||||||
|
|
||||||
if (e[typeSymbol] === NotificationType.Zap) {
|
if (e[typeSymbol] === NotificationType.Zap) {
|
||||||
if (!showZaps) return false;
|
if (!showZaps) return false;
|
||||||
@@ -70,11 +72,44 @@ const NotificationsTimeline = memo(
|
|||||||
showReactions,
|
showReactions,
|
||||||
showReposts,
|
showReposts,
|
||||||
showZaps,
|
showZaps,
|
||||||
minTimestamp,
|
// minTimestamp,
|
||||||
maxTimestamp,
|
// maxTimestamp,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// VIM controls
|
||||||
|
const { id: focused, focus: setFocus } = useContext(FocusedContext);
|
||||||
|
const navigatePrev = () => {
|
||||||
|
const focusedEvent = filteredEvents.find((e) => e.id === focused);
|
||||||
|
|
||||||
|
if (focusedEvent) {
|
||||||
|
const i = filteredEvents.indexOf(focusedEvent);
|
||||||
|
if (i >= 1) {
|
||||||
|
const prev = filteredEvents[i - 1];
|
||||||
|
if (prev) setFocus(prev.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const navigateNext = () => {
|
||||||
|
const focusedEvent = filteredEvents.find((e) => e.id === focused);
|
||||||
|
|
||||||
|
if (focusedEvent) {
|
||||||
|
const i = filteredEvents.indexOf(focusedEvent);
|
||||||
|
if (i !== -1 && i < filteredEvents.length - 2) {
|
||||||
|
const next = filteredEvents[i + 1];
|
||||||
|
if (next) setFocus(next.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useKeyPressEvent("ArrowUp", navigatePrev);
|
||||||
|
useKeyPressEvent("ArrowDown", navigateNext);
|
||||||
|
useKeyPressEvent("k", navigatePrev);
|
||||||
|
useKeyPressEvent("h", navigatePrev);
|
||||||
|
useKeyPressEvent("j", navigateNext);
|
||||||
|
useKeyPressEvent("l", navigateNext);
|
||||||
|
useKeyPressEvent("H", () => setFocus(filteredEvents[0]?.id ?? ""));
|
||||||
|
useKeyPressEvent("L", () => setFocus(filteredEvents[filteredEvents.length - 1]?.id ?? ""));
|
||||||
|
|
||||||
if (filteredEvents.length === 0)
|
if (filteredEvents.length === 0)
|
||||||
return (
|
return (
|
||||||
<Flex alignItems="center" justifyContent="center" minH="25vh" fontWeight="bold" fontSize="4xl">
|
<Flex alignItems="center" justifyContent="center" minH="25vh" fontWeight="bold" fontSize="4xl">
|
||||||
@@ -82,69 +117,86 @@ const NotificationsTimeline = memo(
|
|||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const items: ReactNode[] = [];
|
||||||
<>
|
|
||||||
{filteredEvents.map((event) => (
|
let prev = dayjs();
|
||||||
<NotificationItem key={event.id} event={event} />
|
for (const event of filteredEvents) {
|
||||||
))}
|
// insert markers at every day
|
||||||
</>
|
if (prev.diff(dayjs.unix(event.created_at), "d") > 0) {
|
||||||
);
|
prev = dayjs.unix(event.created_at);
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
<Flex gap="4" p="2" key={prev.unix() + "-marker"} alignItems="center">
|
||||||
|
<Divider />
|
||||||
|
<Text whiteSpace="pre">{prev.fromNow()}</Text>
|
||||||
|
<Divider />
|
||||||
|
</Flex>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(<NotificationItem key={event.id} event={event} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{items}</>;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function NotificationsPage() {
|
function NotificationsPage() {
|
||||||
const { timeline } = useNotifications();
|
const { timeline } = useNotifications();
|
||||||
|
|
||||||
|
const { value: focused, setValue: setFocused } = useRouteStateValue("focused", "");
|
||||||
|
const focusContext = useMemo(() => ({ id: focused, focus: setFocused }), [focused, setFocused]);
|
||||||
|
|
||||||
const showReplies = useLocalStorageDisclosure("notifications-show-replies", true);
|
const showReplies = useLocalStorageDisclosure("notifications-show-replies", true);
|
||||||
const showMentions = useLocalStorageDisclosure("notifications-show-mentions", true);
|
const showMentions = useLocalStorageDisclosure("notifications-show-mentions", true);
|
||||||
const showZaps = useLocalStorageDisclosure("notifications-show-zaps", true);
|
const showZaps = useLocalStorageDisclosure("notifications-show-zaps", true);
|
||||||
const showReposts = useLocalStorageDisclosure("notifications-show-reposts", true);
|
const showReposts = useLocalStorageDisclosure("notifications-show-reposts", true);
|
||||||
const showReactions = useLocalStorageDisclosure("notifications-show-reactions", true);
|
const showReactions = useLocalStorageDisclosure("notifications-show-reactions", true);
|
||||||
|
|
||||||
const today = dayjs().format(DATE_FORMAT);
|
// const today = dayjs().format(DATE_FORMAT);
|
||||||
const { value: day, setValue: setDay } = useRouteSearchValue(
|
// const { value: day, setValue: setDay } = useRouteSearchValue(
|
||||||
"date",
|
// "date",
|
||||||
timeline.timeline.value[0] ? dayjs.unix(timeline.timeline.value[0].created_at).format(DATE_FORMAT) : today,
|
// timeline.timeline.value[0] ? dayjs.unix(timeline.timeline.value[0].created_at).format(DATE_FORMAT) : today,
|
||||||
);
|
// );
|
||||||
|
|
||||||
const nextDay = () => {
|
// const nextDay = () => {
|
||||||
setDay((date) => {
|
// setDay((date) => {
|
||||||
const endOfDay = dayjs(date ?? today, DATE_FORMAT)
|
// const endOfDay = dayjs(date ?? today, DATE_FORMAT)
|
||||||
.endOf("day")
|
// .endOf("day")
|
||||||
.unix();
|
// .unix();
|
||||||
|
|
||||||
// find the next event
|
// // find the next event
|
||||||
for (let i = timeline.timeline.value.length - 1; i > 0; i--) {
|
// for (let i = timeline.timeline.value.length - 1; i > 0; i--) {
|
||||||
const e = timeline.timeline.value[i];
|
// const e = timeline.timeline.value[i];
|
||||||
if (e.created_at > endOfDay) return dayjs.unix(e.created_at).format(DATE_FORMAT);
|
// if (e.created_at > endOfDay) return dayjs.unix(e.created_at).format(DATE_FORMAT);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return dayjs(date ?? today, DATE_FORMAT)
|
// return dayjs(date ?? today, DATE_FORMAT)
|
||||||
.add(1, "day")
|
// .add(1, "day")
|
||||||
.format(DATE_FORMAT);
|
// .format(DATE_FORMAT);
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
const previousDay = () => {
|
// const previousDay = () => {
|
||||||
setDay((date) => {
|
// setDay((date) => {
|
||||||
const startOfDay = dayjs(date ?? today, DATE_FORMAT).unix();
|
// const startOfDay = dayjs(date ?? today, DATE_FORMAT).unix();
|
||||||
|
|
||||||
// find the next event
|
// // find the next event
|
||||||
for (const e of timeline.timeline.value) {
|
// for (const e of timeline.timeline.value) {
|
||||||
if (e.created_at < startOfDay) return dayjs.unix(e.created_at).format(DATE_FORMAT);
|
// if (e.created_at < startOfDay) return dayjs.unix(e.created_at).format(DATE_FORMAT);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return dayjs(date ?? today, DATE_FORMAT)
|
// return dayjs(date ?? today, DATE_FORMAT)
|
||||||
.subtract(1, "day")
|
// .subtract(1, "day")
|
||||||
.format(DATE_FORMAT);
|
// .format(DATE_FORMAT);
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalPageLayout>
|
<VerticalPageLayout>
|
||||||
<Flex direction={{ base: "column", lg: "row-reverse" }} gap="2" justifyContent="space-between">
|
<Flex direction={{ base: "column", lg: "row-reverse" }} gap="2" justifyContent="space-between">
|
||||||
<Flex gap="2" justifyContent="space-between">
|
{/* <Flex gap="2" justifyContent="space-between">
|
||||||
<IconButton aria-label="Previous" icon={<ChevronLeftIcon boxSize={6} />} onClick={previousDay} />
|
<IconButton aria-label="Previous" icon={<ChevronLeftIcon boxSize={6} />} onClick={previousDay} />
|
||||||
<Input
|
<Input
|
||||||
maxW="xs"
|
maxW="xs"
|
||||||
@@ -160,7 +212,7 @@ function NotificationsPage() {
|
|||||||
onClick={nextDay}
|
onClick={nextDay}
|
||||||
isDisabled={day === today}
|
isDisabled={day === today}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex> */}
|
||||||
|
|
||||||
<Flex gap="2" wrap="wrap" flex={1}>
|
<Flex gap="2" wrap="wrap" flex={1}>
|
||||||
<NotificationTypeToggles
|
<NotificationTypeToggles
|
||||||
@@ -180,19 +232,23 @@ function NotificationsPage() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<IntersectionObserverProvider callback={callback}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<NotificationsTimeline
|
<FocusedContext.Provider value={focusContext}>
|
||||||
day={day}
|
<Flex direction="column">
|
||||||
showReplies={showReplies.isOpen}
|
<NotificationsTimeline
|
||||||
showMentions={showMentions.isOpen}
|
// day={day}
|
||||||
showZaps={showZaps.isOpen}
|
showReplies={showReplies.isOpen}
|
||||||
showReposts={showReposts.isOpen}
|
showMentions={showMentions.isOpen}
|
||||||
showReactions={showReactions.isOpen}
|
showZaps={showZaps.isOpen}
|
||||||
/>
|
showReposts={showReposts.isOpen}
|
||||||
|
showReactions={showReactions.isOpen}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</FocusedContext.Provider>
|
||||||
</IntersectionObserverProvider>
|
</IntersectionObserverProvider>
|
||||||
|
|
||||||
{/* <TimelineActionAndStatus timeline={timeline} /> */}
|
<TimelineActionAndStatus timeline={timeline} />
|
||||||
|
|
||||||
<ButtonGroup mx="auto" mt="4">
|
{/* <ButtonGroup mx="auto" mt="4">
|
||||||
<Button leftIcon={<ChevronLeftIcon boxSize={6} />} onClick={previousDay}>
|
<Button leftIcon={<ChevronLeftIcon boxSize={6} />} onClick={previousDay}>
|
||||||
Previous
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
@@ -201,7 +257,7 @@ function NotificationsPage() {
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ButtonGroup>
|
</ButtonGroup> */}
|
||||||
</VerticalPageLayout>
|
</VerticalPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -170,16 +170,16 @@ export default function DisplaySettings() {
|
|||||||
<span>Be careful its easy to hide all notes if you add common words.</span>
|
<span>Be careful its easy to hide all notes if you add common words.</span>
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
ml="auto"
|
||||||
|
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
|
||||||
|
isDisabled={!formState.isDirty}
|
||||||
|
colorScheme="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save Settings
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Button
|
|
||||||
ml="auto"
|
|
||||||
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
|
|
||||||
isDisabled={!formState.isDirty}
|
|
||||||
colorScheme="primary"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Save Settings
|
|
||||||
</Button>
|
|
||||||
</VerticalPageLayout>
|
</VerticalPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -114,16 +114,16 @@ export default function PerformanceSettings() {
|
|||||||
<FormHelperText>Enabled: automatically decrypt direct messages</FormHelperText>
|
<FormHelperText>Enabled: automatically decrypt direct messages</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<VerifyEventSettings />
|
<VerifyEventSettings />
|
||||||
|
<Button
|
||||||
|
ml="auto"
|
||||||
|
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
|
||||||
|
isDisabled={!formState.isDirty}
|
||||||
|
colorScheme="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save Settings
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Button
|
|
||||||
ml="auto"
|
|
||||||
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
|
|
||||||
isDisabled={!formState.isDirty}
|
|
||||||
colorScheme="primary"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Save Settings
|
|
||||||
</Button>
|
|
||||||
</VerticalPageLayout>
|
</VerticalPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -183,16 +183,16 @@ export default function PostSettings() {
|
|||||||
client tag to events
|
client tag to events
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
ml="auto"
|
||||||
|
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
|
||||||
|
isDisabled={!formState.isDirty}
|
||||||
|
colorScheme="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save Settings
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Button
|
|
||||||
ml="auto"
|
|
||||||
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
|
|
||||||
isDisabled={!formState.isDirty}
|
|
||||||
colorScheme="primary"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Save Settings
|
|
||||||
</Button>
|
|
||||||
</VerticalPageLayout>
|
</VerticalPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -188,16 +188,16 @@ export default function PrivacySettings() {
|
|||||||
</span>
|
</span>
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
ml="auto"
|
||||||
|
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
|
||||||
|
isDisabled={!formState.isDirty}
|
||||||
|
colorScheme="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save Settings
|
||||||
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Button
|
|
||||||
ml="auto"
|
|
||||||
isLoading={formState.isLoading || formState.isValidating || formState.isSubmitting}
|
|
||||||
isDisabled={!formState.isDirty}
|
|
||||||
colorScheme="primary"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Save Settings
|
|
||||||
</Button>
|
|
||||||
</VerticalPageLayout>
|
</VerticalPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user