mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-06-21 22:32:36 +02:00
show notifications on timeline
This commit is contained in:
parent
5b52792236
commit
1eb6c498a6
5
.changeset/tame-files-worry.md
Normal file
5
.changeset/tame-files-worry.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Show notifications on launchpad
|
145
src/classes/notifications.ts
Normal file
145
src/classes/notifications.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { NostrEvent, kinds, nip18, nip25 } from "nostr-tools";
|
||||||
|
import _throttle from "lodash.throttle";
|
||||||
|
|
||||||
|
import EventStore from "./event-store";
|
||||||
|
import { PersistentSubject } from "./subject";
|
||||||
|
import { getThreadReferences, isPTagMentionedInContent, isReply, isRepost } from "../helpers/nostr/event";
|
||||||
|
import { getParsedZap } from "../helpers/nostr/zaps";
|
||||||
|
import singleEventService from "../services/single-event";
|
||||||
|
import RelaySet from "./relay-set";
|
||||||
|
import clientRelaysService from "../services/client-relays";
|
||||||
|
import { getPubkeysMentionedInContent } from "../helpers/nostr/post";
|
||||||
|
import { TORRENT_COMMENT_KIND } from "../helpers/nostr/torrents";
|
||||||
|
import { STREAM_CHAT_MESSAGE_KIND } from "../helpers/nostr/stream";
|
||||||
|
|
||||||
|
export const typeSymbol = Symbol("notificationType");
|
||||||
|
|
||||||
|
export enum NotificationType {
|
||||||
|
Reply = "reply",
|
||||||
|
Repost = "repost",
|
||||||
|
Zap = "zap",
|
||||||
|
Reaction = "reaction",
|
||||||
|
Mention = "mention",
|
||||||
|
}
|
||||||
|
export type CategorizedEvent = NostrEvent & { [typeSymbol]?: NotificationType };
|
||||||
|
|
||||||
|
export default class AccountNotifications {
|
||||||
|
store: EventStore;
|
||||||
|
pubkey: string;
|
||||||
|
private subs: ZenObservable.Subscription[] = [];
|
||||||
|
|
||||||
|
timeline = new PersistentSubject<CategorizedEvent[]>([]);
|
||||||
|
|
||||||
|
constructor(pubkey: string, store: EventStore) {
|
||||||
|
this.store = store;
|
||||||
|
this.pubkey = pubkey;
|
||||||
|
|
||||||
|
this.subs.push(store.onEvent.subscribe(this.handleEvent.bind(this)));
|
||||||
|
|
||||||
|
for (const [_, event] of store.events) this.handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private categorizeEvent(event: NostrEvent): CategorizedEvent {
|
||||||
|
const e = event as CategorizedEvent;
|
||||||
|
if (event.kind === kinds.Zap) {
|
||||||
|
e[typeSymbol] = NotificationType.Zap;
|
||||||
|
} else if (event.kind === kinds.Reaction) {
|
||||||
|
e[typeSymbol] = NotificationType.Reaction;
|
||||||
|
} else if (isRepost(event)) {
|
||||||
|
e[typeSymbol] = NotificationType.Repost;
|
||||||
|
} else if (
|
||||||
|
event.kind === kinds.ShortTextNote ||
|
||||||
|
event.kind === TORRENT_COMMENT_KIND ||
|
||||||
|
event.kind === STREAM_CHAT_MESSAGE_KIND ||
|
||||||
|
event.kind === kinds.LongFormArticle
|
||||||
|
) {
|
||||||
|
// is the "p" tag directly mentioned in the content
|
||||||
|
const isMentioned = isPTagMentionedInContent(event, this.pubkey);
|
||||||
|
// is the pubkey mentioned in any way in the content
|
||||||
|
const isQuoted = !isMentioned && getPubkeysMentionedInContent(event.content).includes(this.pubkey);
|
||||||
|
|
||||||
|
if (isMentioned || isQuoted) e[typeSymbol] = NotificationType.Mention;
|
||||||
|
if (isReply(event)) e[typeSymbol] = NotificationType.Reply;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEvent(event: NostrEvent) {
|
||||||
|
const e = this.categorizeEvent(event);
|
||||||
|
|
||||||
|
const getAndSubscribe = (eventId: string, relays?: string[]) => {
|
||||||
|
const subject = singleEventService.requestEvent(
|
||||||
|
eventId,
|
||||||
|
RelaySet.from(clientRelaysService.readRelays.value, relays),
|
||||||
|
);
|
||||||
|
|
||||||
|
subject.once(this.throttleUpdateTimeline);
|
||||||
|
return subject.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (e[typeSymbol]) {
|
||||||
|
case NotificationType.Reply:
|
||||||
|
const refs = getThreadReferences(e);
|
||||||
|
if (refs.reply?.e?.id) getAndSubscribe(refs.reply.e.id, refs.reply.e.relays);
|
||||||
|
break;
|
||||||
|
case NotificationType.Reaction: {
|
||||||
|
const pointer = nip25.getReactedEventPointer(e);
|
||||||
|
if (pointer?.id) getAndSubscribe(pointer.id, pointer.relays);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throttleUpdateTimeline = _throttle(this.updateTimeline.bind(this), 200);
|
||||||
|
updateTimeline() {
|
||||||
|
const sorted = this.store.getSortedEvents();
|
||||||
|
|
||||||
|
const timeline: CategorizedEvent[] = [];
|
||||||
|
for (const event of sorted) {
|
||||||
|
if (!Object.hasOwn(event, typeSymbol)) continue;
|
||||||
|
const e = event as CategorizedEvent;
|
||||||
|
|
||||||
|
switch (e[typeSymbol]) {
|
||||||
|
case NotificationType.Reply:
|
||||||
|
const refs = getThreadReferences(e);
|
||||||
|
if (!refs.reply?.e?.id) break;
|
||||||
|
if (refs.reply?.e?.author && refs.reply?.e?.author !== this.pubkey) break;
|
||||||
|
const parent = singleEventService.getSubject(refs.reply.e.id).value;
|
||||||
|
if (!parent || parent.pubkey !== this.pubkey) break;
|
||||||
|
timeline.push(e);
|
||||||
|
break;
|
||||||
|
case NotificationType.Mention:
|
||||||
|
timeline.push(e);
|
||||||
|
break;
|
||||||
|
case NotificationType.Repost: {
|
||||||
|
const pointer = nip18.getRepostedEventPointer(e);
|
||||||
|
if (pointer?.author !== this.pubkey) break;
|
||||||
|
timeline.push(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case NotificationType.Reaction: {
|
||||||
|
const pointer = nip25.getReactedEventPointer(e);
|
||||||
|
if (!pointer) break;
|
||||||
|
if (pointer.author !== this.pubkey) break;
|
||||||
|
if (pointer.kind === kinds.EncryptedDirectMessage) break;
|
||||||
|
const parent = singleEventService.getSubject(pointer.id).value;
|
||||||
|
if (parent && parent.kind === kinds.EncryptedDirectMessage) break;
|
||||||
|
timeline.push(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case NotificationType.Zap:
|
||||||
|
const parsed = getParsedZap(e);
|
||||||
|
if (parsed instanceof Error) break;
|
||||||
|
if (!parsed.payment.amount) break;
|
||||||
|
timeline.push(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.timeline.next(timeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
for (const sub of this.subs) sub.unsubscribe();
|
||||||
|
this.subs = [];
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,16 @@ export default class Subject<T> {
|
|||||||
}
|
}
|
||||||
subscribe: Observable<T>["subscribe"];
|
subscribe: Observable<T>["subscribe"];
|
||||||
|
|
||||||
|
once(next: (value: T) => void) {
|
||||||
|
const sub = this.subscribe((v) => {
|
||||||
|
if (v !== undefined) {
|
||||||
|
next(v);
|
||||||
|
sub.unsubscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
map<R>(callback: (value: T) => R, defaultValue?: R): Subject<R> {
|
map<R>(callback: (value: T) => R, defaultValue?: R): Subject<R> {
|
||||||
const child = new Subject(defaultValue);
|
const child = new Subject(defaultValue);
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
import { IconProps, useColorMode } from "@chakra-ui/react";
|
import { IconProps, useColorMode } from "@chakra-ui/react";
|
||||||
import { QuestionIcon, QuestionOutlineIcon } from "@chakra-ui/icons";
|
|
||||||
|
|
||||||
import useDnsIdentity from "../../hooks/use-dns-identity";
|
import useDnsIdentity from "../../hooks/use-dns-identity";
|
||||||
import useUserMetadata from "../../hooks/use-user-metadata";
|
import useUserMetadata from "../../hooks/use-user-metadata";
|
||||||
@ -25,7 +25,7 @@ export function useDnsIdentityColor(pubkey: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UserDnsIdentityIcon({ pubkey, ...props }: { pubkey: string } & IconProps) {
|
const UserDnsIdentityIcon = forwardRef<SVGSVGElement, { pubkey: string } & IconProps>(({ pubkey, ...props }, ref) => {
|
||||||
const metadata = useUserMetadata(pubkey);
|
const metadata = useUserMetadata(pubkey);
|
||||||
const identity = useDnsIdentity(metadata?.nip05);
|
const identity = useDnsIdentity(metadata?.nip05);
|
||||||
|
|
||||||
@ -34,12 +34,13 @@ export default function UserDnsIdentityIcon({ pubkey, ...props }: { pubkey: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (identity === undefined) {
|
if (identity === undefined) {
|
||||||
return <VerificationFailed color="yellow.500" {...props} />;
|
return <VerificationFailed color="yellow.500" {...props} ref={ref} />;
|
||||||
} else if (identity === null) {
|
} else if (identity === null) {
|
||||||
return <VerificationMissing color="red.500" {...props} />;
|
return <VerificationMissing color="red.500" {...props} ref={ref} />;
|
||||||
} else if (pubkey === identity.pubkey) {
|
} else if (pubkey === identity.pubkey) {
|
||||||
return <VerifiedIcon color="purple.500" {...props} />;
|
return <VerifiedIcon color="purple.500" {...props} ref={ref} />;
|
||||||
} else {
|
} else {
|
||||||
return <VerificationFailed color="red.500" {...props} />;
|
return <VerificationFailed color="red.500" {...props} ref={ref} />;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
export default UserDnsIdentityIcon;
|
||||||
|
@ -39,22 +39,22 @@ export function pointerMatchEvent(event: NostrEvent, pointer: AddressPointer | E
|
|||||||
|
|
||||||
const isReplySymbol = Symbol("isReply");
|
const isReplySymbol = Symbol("isReply");
|
||||||
export function isReply(event: NostrEvent | DraftNostrEvent) {
|
export function isReply(event: NostrEvent | DraftNostrEvent) {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
if (event[isReplySymbol] !== undefined) return event[isReplySymbol] as boolean;
|
if (event[isReplySymbol] !== undefined) return event[isReplySymbol] as boolean;
|
||||||
|
|
||||||
if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) return false;
|
if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) return false;
|
||||||
const isReply = !!getThreadReferences(event).reply;
|
const isReply = !!getThreadReferences(event).reply;
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
event[isReplySymbol] = isReply;
|
event[isReplySymbol] = isReply;
|
||||||
return isReply;
|
return isReply;
|
||||||
}
|
}
|
||||||
export function isMentionedInContent(event: NostrEvent | DraftNostrEvent, pubkey: string) {
|
export function isPTagMentionedInContent(event: NostrEvent | DraftNostrEvent, pubkey: string) {
|
||||||
return filterTagsByContentRefs(event.content, event.tags).some((t) => t[1] === pubkey);
|
return filterTagsByContentRefs(event.content, event.tags).some((t) => t[1] === pubkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRepostSymbol = Symbol("isRepost");
|
const isRepostSymbol = Symbol("isRepost");
|
||||||
export function isRepost(event: NostrEvent | DraftNostrEvent) {
|
export function isRepost(event: NostrEvent | DraftNostrEvent) {
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
if (event[isRepostSymbol] !== undefined) return event[isRepostSymbol] as boolean;
|
if (event[isRepostSymbol] !== undefined) return event[isRepostSymbol] as boolean;
|
||||||
|
|
||||||
if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) return true;
|
if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) return true;
|
||||||
@ -62,7 +62,7 @@ export function isRepost(event: NostrEvent | DraftNostrEvent) {
|
|||||||
const match = event.content.match(getMatchNostrLink());
|
const match = event.content.match(getMatchNostrLink());
|
||||||
const isRepost = !!match && match[0].length === event.content.length;
|
const isRepost = !!match && match[0].length === event.content.length;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
event[isRepostSymbol] = isRepost;
|
event[isRepostSymbol] = isRepost;
|
||||||
return isRepost;
|
return isRepost;
|
||||||
}
|
}
|
||||||
@ -165,20 +165,7 @@ export function interpretThreadTags(event: NostrEvent | DraftNostrEvent) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventReferences = ReturnType<typeof getThreadReferences>;
|
export type ThreadReferences = {
|
||||||
export function getThreadReferences(event: NostrEvent | DraftNostrEvent) {
|
|
||||||
const tags = interpretThreadTags(event);
|
|
||||||
|
|
||||||
return {
|
|
||||||
root: tags.root && {
|
|
||||||
e: tags.root.e && eTagToEventPointer(tags.root.e),
|
|
||||||
a: tags.root.a && aTagToAddressPointer(tags.root.a),
|
|
||||||
},
|
|
||||||
reply: tags.reply && {
|
|
||||||
e: tags.reply.e && eTagToEventPointer(tags.reply.e),
|
|
||||||
a: tags.reply.a && aTagToAddressPointer(tags.reply.a),
|
|
||||||
},
|
|
||||||
} as {
|
|
||||||
root?:
|
root?:
|
||||||
| { e: EventPointer; a: undefined }
|
| { e: EventPointer; a: undefined }
|
||||||
| { e: undefined; a: AddressPointer }
|
| { e: undefined; a: AddressPointer }
|
||||||
@ -187,7 +174,32 @@ export function getThreadReferences(event: NostrEvent | DraftNostrEvent) {
|
|||||||
| { e: EventPointer; a: undefined }
|
| { e: EventPointer; a: undefined }
|
||||||
| { e: undefined; a: AddressPointer }
|
| { e: undefined; a: AddressPointer }
|
||||||
| { e: EventPointer; a: AddressPointer };
|
| { e: EventPointer; a: AddressPointer };
|
||||||
};
|
};
|
||||||
|
export const threadRefsSymbol = Symbol("threadRefs");
|
||||||
|
export type EventWithThread = (NostrEvent | DraftNostrEvent) & { [threadRefsSymbol]: ThreadReferences };
|
||||||
|
|
||||||
|
export function getThreadReferences(event: NostrEvent | DraftNostrEvent): ThreadReferences {
|
||||||
|
// @ts-expect-error
|
||||||
|
if (Object.hasOwn(event, threadRefsSymbol)) return event[threadRefsSymbol];
|
||||||
|
|
||||||
|
const e = event as EventWithThread;
|
||||||
|
const tags = interpretThreadTags(e);
|
||||||
|
|
||||||
|
const threadRef = {
|
||||||
|
root: tags.root && {
|
||||||
|
e: tags.root.e && eTagToEventPointer(tags.root.e),
|
||||||
|
a: tags.root.a && aTagToAddressPointer(tags.root.a),
|
||||||
|
},
|
||||||
|
reply: tags.reply && {
|
||||||
|
e: tags.reply.e && eTagToEventPointer(tags.reply.e),
|
||||||
|
a: tags.reply.a && aTagToAddressPointer(tags.reply.a),
|
||||||
|
},
|
||||||
|
} as ThreadReferences;
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
event[threadRefsSymbol] = threadRef;
|
||||||
|
|
||||||
|
return threadRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEventCoordinate(event: NostrEvent) {
|
export function getEventCoordinate(event: NostrEvent) {
|
||||||
|
@ -3,7 +3,7 @@ import { isETag, isPTag, NostrEvent } from "../../types/nostr-event";
|
|||||||
import { ParsedInvoice, parsePaymentRequest } from "../bolt11";
|
import { ParsedInvoice, parsePaymentRequest } from "../bolt11";
|
||||||
|
|
||||||
import { Kind0ParsedContent } from "./user-metadata";
|
import { Kind0ParsedContent } from "./user-metadata";
|
||||||
import { utils } from "nostr-tools";
|
import { nip57, utils } from "nostr-tools";
|
||||||
|
|
||||||
// based on https://github.com/nbd-wtf/nostr-tools/blob/master/nip57.ts
|
// based on https://github.com/nbd-wtf/nostr-tools/blob/master/nip57.ts
|
||||||
export async function getZapEndpoint(metadata: Kind0ParsedContent): Promise<null | string> {
|
export async function getZapEndpoint(metadata: Kind0ParsedContent): Promise<null | string> {
|
||||||
@ -52,6 +52,20 @@ export type ParsedZap = {
|
|||||||
eventId?: string;
|
eventId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parsedZapSymbol = Symbol("parsedZap");
|
||||||
|
type ParsedZapEvent = NostrEvent & { [parsedZapSymbol]: ParsedZap | Error };
|
||||||
|
export function getParsedZap(event: NostrEvent) {
|
||||||
|
const e = event as ParsedZapEvent;
|
||||||
|
if (Object.hasOwn(e, parsedZapSymbol)) return e[parsedZapSymbol];
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (e[parsedZapSymbol] = parseZapEvent(e));
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) return (e[parsedZapSymbol] = error);
|
||||||
|
else throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function parseZapEvent(event: NostrEvent): ParsedZap {
|
export function parseZapEvent(event: NostrEvent): ParsedZap {
|
||||||
const zapRequestStr = event.tags.find(([t, v]) => t === "description")?.[1];
|
const zapRequestStr = event.tags.find(([t, v]) => t === "description")?.[1];
|
||||||
if (!zapRequestStr) throw new Error("no description tag");
|
if (!zapRequestStr) throw new Error("no description tag");
|
||||||
@ -59,10 +73,8 @@ export function parseZapEvent(event: NostrEvent): ParsedZap {
|
|||||||
const bolt11 = event.tags.find((t) => t[0] === "bolt11")?.[1];
|
const bolt11 = event.tags.find((t) => t[0] === "bolt11")?.[1];
|
||||||
if (!bolt11) throw new Error("missing bolt11 invoice");
|
if (!bolt11) throw new Error("missing bolt11 invoice");
|
||||||
|
|
||||||
// TODO: disabled until signature verification can be offloaded to a web worker
|
const error = nip57.validateZapRequest(zapRequestStr);
|
||||||
|
if (error) throw new Error(error);
|
||||||
// const error = nip57.validateZapRequest(zapRequestStr);
|
|
||||||
// if (error) throw new Error(error);
|
|
||||||
|
|
||||||
const request = JSON.parse(zapRequestStr) as NostrEvent;
|
const request = JSON.parse(zapRequestStr) as NostrEvent;
|
||||||
const payment = parsePaymentRequest(bolt11);
|
const payment = parsePaymentRequest(bolt11);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
import { EventReferences, getThreadReferences } from "./nostr/event";
|
import { ThreadReferences, getThreadReferences } from "./nostr/event";
|
||||||
|
|
||||||
export function countReplies(replies: ThreadItem[]): number {
|
export function countReplies(replies: ThreadItem[]): number {
|
||||||
return replies.reduce((c, item) => c + countReplies(item.replies), 0) + replies.length;
|
return replies.reduce((c, item) => c + countReplies(item.replies), 0) + replies.length;
|
||||||
@ -13,7 +13,7 @@ export type ThreadItem = {
|
|||||||
/** the parent event this is replying to */
|
/** the parent event this is replying to */
|
||||||
replyingTo?: ThreadItem;
|
replyingTo?: ThreadItem;
|
||||||
/** refs from nostr event */
|
/** refs from nostr event */
|
||||||
refs: EventReferences;
|
refs: ThreadReferences;
|
||||||
/** direct child replies */
|
/** direct child replies */
|
||||||
replies: ThreadItem[];
|
replies: ThreadItem[];
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import { ChakraProvider, localStorageManager } from "@chakra-ui/react";
|
|||||||
import { SigningProvider } from "./signing-provider";
|
import { SigningProvider } from "./signing-provider";
|
||||||
import buildTheme from "../../theme";
|
import buildTheme from "../../theme";
|
||||||
import useAppSettings from "../../hooks/use-app-settings";
|
import useAppSettings from "../../hooks/use-app-settings";
|
||||||
import NotificationTimelineProvider from "./notification-timeline";
|
import NotificationsProvider from "./notifications";
|
||||||
import { DefaultEmojiProvider, UserEmojiProvider } from "./emoji-provider";
|
import { DefaultEmojiProvider, UserEmojiProvider } from "./emoji-provider";
|
||||||
import { AllUserSearchDirectoryProvider } from "./user-directory-provider";
|
import { AllUserSearchDirectoryProvider } from "./user-directory-provider";
|
||||||
import BreakpointProvider from "./breakpoint-provider";
|
import BreakpointProvider from "./breakpoint-provider";
|
||||||
@ -26,7 +26,7 @@ export const GlobalProviders = ({ children }: { children: React.ReactNode }) =>
|
|||||||
<SigningProvider>
|
<SigningProvider>
|
||||||
<PublishProvider>
|
<PublishProvider>
|
||||||
<DecryptionProvider>
|
<DecryptionProvider>
|
||||||
<NotificationTimelineProvider>
|
<NotificationsProvider>
|
||||||
<DMTimelineProvider>
|
<DMTimelineProvider>
|
||||||
<DefaultEmojiProvider>
|
<DefaultEmojiProvider>
|
||||||
<UserEmojiProvider>
|
<UserEmojiProvider>
|
||||||
@ -34,7 +34,7 @@ export const GlobalProviders = ({ children }: { children: React.ReactNode }) =>
|
|||||||
</UserEmojiProvider>
|
</UserEmojiProvider>
|
||||||
</DefaultEmojiProvider>
|
</DefaultEmojiProvider>
|
||||||
</DMTimelineProvider>
|
</DMTimelineProvider>
|
||||||
</NotificationTimelineProvider>
|
</NotificationsProvider>
|
||||||
</DecryptionProvider>
|
</DecryptionProvider>
|
||||||
</PublishProvider>
|
</PublishProvider>
|
||||||
</SigningProvider>
|
</SigningProvider>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react";
|
import { PropsWithChildren, createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { kinds } from "nostr-tools";
|
import { kinds } from "nostr-tools";
|
||||||
|
|
||||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||||
@ -9,25 +9,27 @@ import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
|
|||||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||||
import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents";
|
import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents";
|
||||||
import { useUserInbox } from "../../hooks/use-user-mailboxes";
|
import { useUserInbox } from "../../hooks/use-user-mailboxes";
|
||||||
|
import AccountNotifications from "../../classes/notifications";
|
||||||
|
|
||||||
type NotificationTimelineContextType = {
|
type NotificationTimelineContextType = {
|
||||||
timeline?: TimelineLoader;
|
timeline: TimelineLoader;
|
||||||
|
notifications?: AccountNotifications;
|
||||||
};
|
};
|
||||||
const NotificationTimelineContext = createContext<NotificationTimelineContextType>({});
|
const NotificationTimelineContext = createContext<NotificationTimelineContextType | null>(null);
|
||||||
|
|
||||||
export function useNotificationTimeline() {
|
export function useNotifications() {
|
||||||
const context = useContext(NotificationTimelineContext);
|
const ctx = useContext(NotificationTimelineContext);
|
||||||
|
if (!ctx) throw new Error("Missing notifications provider");
|
||||||
if (!context?.timeline) throw new Error("No notification timeline");
|
return ctx;
|
||||||
|
|
||||||
return context.timeline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NotificationTimelineProvider({ children }: PropsWithChildren) {
|
export default function NotificationsProvider({ children }: PropsWithChildren) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const inbox = useUserInbox(account?.pubkey);
|
const inbox = useUserInbox(account?.pubkey);
|
||||||
const readRelays = useReadRelays(inbox);
|
const readRelays = useReadRelays(inbox);
|
||||||
|
|
||||||
|
const [notifications, setNotifications] = useState<AccountNotifications>();
|
||||||
|
|
||||||
const userMuteFilter = useClientSideMuteFilter();
|
const userMuteFilter = useClientSideMuteFilter();
|
||||||
const eventFilter = useCallback(
|
const eventFilter = useCallback(
|
||||||
(event: NostrEvent) => {
|
(event: NostrEvent) => {
|
||||||
@ -57,7 +59,22 @@ export default function NotificationTimelineProvider({ children }: PropsWithChil
|
|||||||
{ eventFilter },
|
{ eventFilter },
|
||||||
);
|
);
|
||||||
|
|
||||||
const context = useMemo(() => ({ timeline }), [timeline]);
|
useEffect(() => {
|
||||||
|
if (!account?.pubkey) return;
|
||||||
|
const n = new AccountNotifications(account.pubkey, timeline.events);
|
||||||
|
setNotifications(n);
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// @ts-expect-error
|
||||||
|
window.accountNotifications = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
n.destroy();
|
||||||
|
setNotifications(undefined);
|
||||||
|
};
|
||||||
|
}, [account?.pubkey, timeline.events]);
|
||||||
|
|
||||||
|
const context = useMemo(() => ({ timeline, notifications }), [timeline, notifications]);
|
||||||
|
|
||||||
return <NotificationTimelineContext.Provider value={context}>{children}</NotificationTimelineContext.Provider>;
|
return <NotificationTimelineContext.Provider value={context}>{children}</NotificationTimelineContext.Provider>;
|
||||||
}
|
}
|
@ -15,6 +15,10 @@ class SingleEventService {
|
|||||||
pending = new Map<string, string[]>();
|
pending = new Map<string, string[]>();
|
||||||
log = logger.extend("SingleEvent");
|
log = logger.extend("SingleEvent");
|
||||||
|
|
||||||
|
getSubject(id: string) {
|
||||||
|
return this.subjects.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
requestEvent(id: string, relays: Iterable<string>) {
|
requestEvent(id: string, relays: Iterable<string>) {
|
||||||
const subject = this.subjects.get(id);
|
const subject = this.subjects.get(id);
|
||||||
if (subject.value) return subject;
|
if (subject.value) return subject;
|
||||||
|
@ -1,13 +1,29 @@
|
|||||||
import { Card, CardBody, CardHeader, CardProps, Heading, Link } from "@chakra-ui/react";
|
import { Button, Card, CardBody, CardHeader, CardProps, Heading, Link } from "@chakra-ui/react";
|
||||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import KeyboardShortcut from "../../../components/keyboard-shortcut";
|
import KeyboardShortcut from "../../../components/keyboard-shortcut";
|
||||||
|
import { useNotifications } from "../../../providers/global/notifications";
|
||||||
|
import useSubject from "../../../hooks/use-subject";
|
||||||
|
import { NotificationType, typeSymbol } from "../../../classes/notifications";
|
||||||
|
import NotificationItem from "../../notifications/components/notification-item";
|
||||||
|
|
||||||
export default function NotificationsCard({ ...props }: Omit<CardProps, "children">) {
|
export default function NotificationsCard({ ...props }: Omit<CardProps, "children">) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
const events =
|
||||||
|
useSubject(notifications?.timeline)?.filter(
|
||||||
|
(event) =>
|
||||||
|
event[typeSymbol] === NotificationType.Mention ||
|
||||||
|
event[typeSymbol] === NotificationType.Reply ||
|
||||||
|
event[typeSymbol] === NotificationType.Zap,
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
const limit = events.length > 20 ? events.slice(0, 20) : events;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="outline" {...props}>
|
<Card variant="outline" {...props}>
|
||||||
<CardHeader display="flex" justifyContent="space-between" alignItems="center">
|
<CardHeader display="flex" justifyContent="space-between" alignItems="center" pb="2">
|
||||||
<Heading size="lg">
|
<Heading size="lg">
|
||||||
<Link as={RouterLink} to="/notifications">
|
<Link as={RouterLink} to="/notifications">
|
||||||
Notifications
|
Notifications
|
||||||
@ -15,8 +31,13 @@ export default function NotificationsCard({ ...props }: Omit<CardProps, "childre
|
|||||||
</Heading>
|
</Heading>
|
||||||
<KeyboardShortcut letter="i" requireMeta ml="auto" onPress={() => navigate("/notifications")} />
|
<KeyboardShortcut letter="i" requireMeta ml="auto" onPress={() => navigate("/notifications")} />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody overflowX="auto" overflowY="hidden" pt="0" display="flex" gap="4">
|
<CardBody overflowX="hidden" overflowY="auto" pt="4" display="flex" gap="2" flexDirection="column" maxH="50vh">
|
||||||
<h1>Nothing here yet</h1>
|
{limit.map((event) => (
|
||||||
|
<NotificationItem event={event} key={event.id} />
|
||||||
|
))}
|
||||||
|
<Button as={RouterLink} to="/notifications" flexShrink={0} variant="link" size="lg" py="6">
|
||||||
|
View More
|
||||||
|
</Button>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,7 @@ import { NostrEvent, isATag, isETag } from "../../../types/nostr-event";
|
|||||||
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
|
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
|
||||||
import { parseZapEvent } from "../../../helpers/nostr/zaps";
|
import { parseZapEvent } from "../../../helpers/nostr/zaps";
|
||||||
import { readablizeSats } from "../../../helpers/bolt11";
|
import { readablizeSats } from "../../../helpers/bolt11";
|
||||||
import { getEventUID, getThreadReferences, isMentionedInContent, parseCoordinate } from "../../../helpers/nostr/event";
|
import { getEventUID, parseCoordinate } from "../../../helpers/nostr/event";
|
||||||
import { EmbedEvent, EmbedEventPointer } from "../../../components/embed-event";
|
import { EmbedEvent, 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";
|
||||||
@ -23,9 +23,8 @@ import {
|
|||||||
RepostIcon,
|
RepostIcon,
|
||||||
} from "../../../components/icons";
|
} from "../../../components/icons";
|
||||||
import useSingleEvent from "../../../hooks/use-single-event";
|
import useSingleEvent from "../../../hooks/use-single-event";
|
||||||
import { TORRENT_COMMENT_KIND } from "../../../helpers/nostr/torrents";
|
|
||||||
import NotificationIconEntry from "./notification-icon-entry";
|
import NotificationIconEntry from "./notification-icon-entry";
|
||||||
import { getPubkeysMentionedInContent } from "../../../helpers/nostr/post";
|
import { CategorizedEvent, NotificationType, typeSymbol } from "../../../classes/notifications";
|
||||||
|
|
||||||
export const ExpandableToggleButton = ({
|
export const ExpandableToggleButton = ({
|
||||||
toggle,
|
toggle,
|
||||||
@ -39,21 +38,6 @@ export const ExpandableToggleButton = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const NoteNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
|
|
||||||
const account = useCurrentAccount()!;
|
|
||||||
const refs = getThreadReferences(event);
|
|
||||||
const parent = useSingleEvent(refs.reply?.e?.id);
|
|
||||||
|
|
||||||
const isReplyingToMe = !!refs.reply?.e?.id && (parent ? parent.pubkey === account.pubkey : true);
|
|
||||||
// is the "p" tag directly mentioned in the content
|
|
||||||
const isMentioned = isMentionedInContent(event, account.pubkey);
|
|
||||||
// is the pubkey mentioned in any way in the content
|
|
||||||
const isQuoted = !isMentioned && getPubkeysMentionedInContent(event.content).includes(account.pubkey);
|
|
||||||
|
|
||||||
if (isReplyingToMe) return <ReplyNotification event={event} ref={ref} />;
|
|
||||||
else if (isMentioned || isQuoted) return <MentionNotification event={event} ref={ref} />;
|
|
||||||
else return null;
|
|
||||||
});
|
|
||||||
const ReplyNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => (
|
const ReplyNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => (
|
||||||
<NotificationIconEntry ref={ref} icon={<ReplyIcon boxSize={8} color="green.400" />}>
|
<NotificationIconEntry ref={ref} icon={<ReplyIcon boxSize={8} color="green.400" />}>
|
||||||
<EmbedEvent event={event} />
|
<EmbedEvent event={event} />
|
||||||
@ -67,11 +51,9 @@ const MentionNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({
|
|||||||
));
|
));
|
||||||
|
|
||||||
const RepostNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
|
const RepostNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
|
||||||
const account = useCurrentAccount()!;
|
|
||||||
const pointer = nip18.getRepostedEventPointer(event);
|
const pointer = nip18.getRepostedEventPointer(event);
|
||||||
const expanded = useDisclosure({ defaultIsOpen: true });
|
const expanded = useDisclosure({ defaultIsOpen: true });
|
||||||
|
if (!pointer) return null;
|
||||||
if (pointer?.author !== account.pubkey) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationIconEntry ref={ref} icon={<RepostIcon boxSize={8} color="blue.400" />}>
|
<NotificationIconEntry ref={ref} icon={<RepostIcon boxSize={8} color="blue.400" />}>
|
||||||
@ -157,24 +139,25 @@ const ZapNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ eve
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const NotificationItem = ({ event }: { event: NostrEvent }) => {
|
const NotificationItem = ({ event }: { event: CategorizedEvent }) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useRegisterIntersectionEntity(ref, getEventUID(event));
|
useRegisterIntersectionEntity(ref, getEventUID(event));
|
||||||
|
|
||||||
let content: ReactNode | null = null;
|
let content: ReactNode | null = null;
|
||||||
switch (event.kind) {
|
switch (event[typeSymbol]) {
|
||||||
case kinds.ShortTextNote:
|
case NotificationType.Reply:
|
||||||
case TORRENT_COMMENT_KIND:
|
content = <ReplyNotification event={event} ref={ref} />;
|
||||||
case kinds.LongFormArticle:
|
|
||||||
content = <NoteNotification event={event} ref={ref} />;
|
|
||||||
break;
|
break;
|
||||||
case kinds.Reaction:
|
case NotificationType.Mention:
|
||||||
|
content = <MentionNotification event={event} ref={ref} />;
|
||||||
|
break;
|
||||||
|
case NotificationType.Reaction:
|
||||||
content = <ReactionNotification event={event} ref={ref} />;
|
content = <ReactionNotification event={event} ref={ref} />;
|
||||||
break;
|
break;
|
||||||
case kinds.Repost:
|
case NotificationType.Repost:
|
||||||
content = <RepostNotification event={event} ref={ref} />;
|
content = <RepostNotification event={event} ref={ref} />;
|
||||||
break;
|
break;
|
||||||
case kinds.Zap:
|
case NotificationType.Zap:
|
||||||
content = <ZapNotification event={event} ref={ref} />;
|
content = <ZapNotification event={event} ref={ref} />;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -12,7 +12,7 @@ import IntersectionObserverProvider, {
|
|||||||
} from "../../providers/local/intersection-observer";
|
} from "../../providers/local/intersection-observer";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
import { useNotificationTimeline } from "../../providers/global/notification-timeline";
|
import { useNotifications } from "../../providers/global/notifications";
|
||||||
import { getEventUID, isReply } from "../../helpers/nostr/event";
|
import { getEventUID, isReply } from "../../helpers/nostr/event";
|
||||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
|
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
|
||||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||||
@ -25,6 +25,7 @@ import DayGroup from "./components/day-group";
|
|||||||
import TimelineLoader from "../../classes/timeline-loader";
|
import TimelineLoader from "../../classes/timeline-loader";
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "../../components/icons";
|
import { ChevronLeftIcon, ChevronRightIcon } from "../../components/icons";
|
||||||
import useRouteSearchValue from "../../hooks/use-route-search-value";
|
import useRouteSearchValue from "../../hooks/use-route-search-value";
|
||||||
|
import { NotificationType, typeSymbol } from "../../classes/notifications";
|
||||||
|
|
||||||
const DATE_FORMAT = "YYYY-MM-DD";
|
const DATE_FORMAT = "YYYY-MM-DD";
|
||||||
|
|
||||||
@ -43,7 +44,6 @@ const NotificationDay = memo(({ day, events }: { day: number; events: NostrEvent
|
|||||||
|
|
||||||
const NotificationsTimeline = memo(
|
const NotificationsTimeline = memo(
|
||||||
({
|
({
|
||||||
timeline,
|
|
||||||
day,
|
day,
|
||||||
showReplies,
|
showReplies,
|
||||||
showMentions,
|
showMentions,
|
||||||
@ -51,7 +51,6 @@ const NotificationsTimeline = memo(
|
|||||||
showReposts,
|
showReposts,
|
||||||
showReactions,
|
showReactions,
|
||||||
}: {
|
}: {
|
||||||
timeline: TimelineLoader;
|
|
||||||
day: string;
|
day: string;
|
||||||
showReplies: boolean;
|
showReplies: boolean;
|
||||||
showMentions: boolean;
|
showMentions: boolean;
|
||||||
@ -59,32 +58,34 @@ const NotificationsTimeline = memo(
|
|||||||
showReposts: boolean;
|
showReposts: boolean;
|
||||||
showReactions: boolean;
|
showReactions: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
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(timeline.timeline);
|
const events = useSubject(notifications?.timeline) ?? [];
|
||||||
|
|
||||||
const throttledEvents = useThrottle(events, 500);
|
|
||||||
const filteredEvents = useMemo(
|
const filteredEvents = useMemo(
|
||||||
() =>
|
() =>
|
||||||
throttledEvents.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 (peoplePubkeys && e.kind !== kinds.Zap && !peoplePubkeys.includes(e.pubkey)) return false;
|
|
||||||
|
|
||||||
if (e.kind === kinds.ShortTextNote) {
|
if (e[typeSymbol] === NotificationType.Zap) {
|
||||||
if (!showReplies && isReply(e)) return false;
|
if (!showZaps) return false;
|
||||||
if (!showMentions && !isReply(e)) return false;
|
if (peoplePubkeys && !peoplePubkeys.includes(e.pubkey)) return false;
|
||||||
}
|
}
|
||||||
if (!showReactions && e.kind === kinds.Reaction) return false;
|
|
||||||
if (!showReposts && (e.kind === kinds.Repost || e.kind === kinds.GenericRepost)) return false;
|
if (!showReplies && e[typeSymbol] === NotificationType.Reply) return false;
|
||||||
if (!showZaps && e.kind === kinds.Zap) return false;
|
if (!showMentions && e[typeSymbol] === NotificationType.Mention) return false;
|
||||||
|
if (!showReactions && e[typeSymbol] === NotificationType.Reaction) return false;
|
||||||
|
if (!showReposts && e[typeSymbol] === NotificationType.Repost) return false;
|
||||||
|
if (!showZaps && e[typeSymbol] === NotificationType.Zap) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
throttledEvents,
|
events,
|
||||||
peoplePubkeys,
|
peoplePubkeys,
|
||||||
showReplies,
|
showReplies,
|
||||||
showMentions,
|
showMentions,
|
||||||
@ -123,6 +124,8 @@ const NotificationsTimeline = memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
function NotificationsPage() {
|
function NotificationsPage() {
|
||||||
|
const { timeline } = useNotifications();
|
||||||
|
|
||||||
const showReplies = useDisclosure({ defaultIsOpen: localStorage.getItem("notifications-show-replies") !== "false" });
|
const showReplies = useDisclosure({ defaultIsOpen: localStorage.getItem("notifications-show-replies") !== "false" });
|
||||||
const showMentions = useDisclosure({
|
const showMentions = useDisclosure({
|
||||||
defaultIsOpen: localStorage.getItem("notifications-show-mentions") !== "false",
|
defaultIsOpen: localStorage.getItem("notifications-show-mentions") !== "false",
|
||||||
@ -134,7 +137,10 @@ function NotificationsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const today = dayjs().format(DATE_FORMAT);
|
const today = dayjs().format(DATE_FORMAT);
|
||||||
const { value: day, setValue: setDay } = useRouteSearchValue("date", dayjs().format(DATE_FORMAT));
|
const { value: day, setValue: setDay } = useRouteSearchValue(
|
||||||
|
"date",
|
||||||
|
timeline.timeline.value[0] ? dayjs.unix(timeline.timeline.value[0].created_at).format(DATE_FORMAT) : today,
|
||||||
|
);
|
||||||
|
|
||||||
const nextDay = () => {
|
const nextDay = () => {
|
||||||
setDay((date) =>
|
setDay((date) =>
|
||||||
@ -143,7 +149,7 @@ function NotificationsPage() {
|
|||||||
.format(DATE_FORMAT),
|
.format(DATE_FORMAT),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const perviousDay = () => {
|
const previousDay = () => {
|
||||||
setDay((date) =>
|
setDay((date) =>
|
||||||
dayjs(date ?? today, DATE_FORMAT)
|
dayjs(date ?? today, DATE_FORMAT)
|
||||||
.subtract(1, "day")
|
.subtract(1, "day")
|
||||||
@ -160,14 +166,13 @@ function NotificationsPage() {
|
|||||||
localStorage.setItem("notifications-show-reactions", String(showReactions.isOpen));
|
localStorage.setItem("notifications-show-reactions", String(showReactions.isOpen));
|
||||||
}, [showReplies.isOpen, showMentions.isOpen, showZaps.isOpen, showReposts.isOpen, showReactions.isOpen]);
|
}, [showReplies.isOpen, showMentions.isOpen, showZaps.isOpen, showReposts.isOpen, showReactions.isOpen]);
|
||||||
|
|
||||||
const timeline = useNotificationTimeline();
|
|
||||||
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="Pervious" icon={<ChevronLeftIcon boxSize={6} />} onClick={perviousDay} />
|
<IconButton aria-label="Previous" icon={<ChevronLeftIcon boxSize={6} />} onClick={previousDay} />
|
||||||
<Input
|
<Input
|
||||||
maxW="xs"
|
maxW="xs"
|
||||||
minW="64"
|
minW="64"
|
||||||
@ -203,7 +208,6 @@ function NotificationsPage() {
|
|||||||
|
|
||||||
<IntersectionObserverProvider callback={callback}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<NotificationsTimeline
|
<NotificationsTimeline
|
||||||
timeline={timeline}
|
|
||||||
day={day}
|
day={day}
|
||||||
showReplies={showReplies.isOpen}
|
showReplies={showReplies.isOpen}
|
||||||
showMentions={showMentions.isOpen}
|
showMentions={showMentions.isOpen}
|
||||||
@ -216,8 +220,8 @@ function NotificationsPage() {
|
|||||||
{/* <TimelineActionAndStatus timeline={timeline} /> */}
|
{/* <TimelineActionAndStatus timeline={timeline} /> */}
|
||||||
|
|
||||||
<ButtonGroup mx="auto" mt="4">
|
<ButtonGroup mx="auto" mt="4">
|
||||||
<Button leftIcon={<ChevronLeftIcon boxSize={6} />} onClick={perviousDay}>
|
<Button leftIcon={<ChevronLeftIcon boxSize={6} />} onClick={previousDay}>
|
||||||
Pervious
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
{day !== today && (
|
{day !== today && (
|
||||||
<Button rightIcon={<ChevronRightIcon boxSize={6} />} onClick={nextDay}>
|
<Button rightIcon={<ChevronRightIcon boxSize={6} />} onClick={nextDay}>
|
||||||
|
@ -8,7 +8,7 @@ import RequireCurrentAccount from "../../providers/route/require-current-account
|
|||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
import { useNotificationTimeline } from "../../providers/global/notification-timeline";
|
import { useNotifications } from "../../providers/global/notifications";
|
||||||
import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents";
|
import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents";
|
||||||
import { groupByRoot } from "../../helpers/notification";
|
import { groupByRoot } from "../../helpers/notification";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
@ -99,7 +99,7 @@ function ThreadsNotificationsPage() {
|
|||||||
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 timeline = useNotificationTimeline();
|
const { timeline } = useNotifications();
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
const events = useSubject(timeline?.timeline);
|
const events = useSubject(timeline?.timeline);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user