From 94b9272042f69fc212e15cecd7c32f90671efb6d Mon Sep 17 00:00:00 2001 From: codytseng Date: Thu, 12 Dec 2024 16:58:37 +0800 Subject: [PATCH] feat: notifications --- .../FormattedTimestamp/index.tsx} | 18 +- src/renderer/src/components/Note/index.tsx | 6 +- .../src/components/NoteList/index.tsx | 7 +- .../components/NotificationButton/index.tsx | 39 ++++ .../src/components/NotificationList/index.tsx | 205 ++++++++++++++++++ .../src/components/ReplyNote/index.tsx | 8 +- src/renderer/src/components/Sidebar/index.tsx | 2 + src/renderer/src/i18n/en.ts | 9 +- src/renderer/src/i18n/index.ts | 6 +- src/renderer/src/i18n/zh.ts | 9 +- .../src/layouts/PrimaryPageLayout/index.tsx | 10 +- .../src/layouts/SecondaryPageLayout/index.tsx | 7 +- src/renderer/src/lib/link.ts | 1 + .../secondary/NotificationListPage/index.tsx | 15 ++ src/renderer/src/providers/NostrProvider.tsx | 1 + src/renderer/src/routes.tsx | 4 +- src/renderer/src/services/client.service.ts | 130 ++++++++++- 17 files changed, 447 insertions(+), 30 deletions(-) rename src/renderer/src/{lib/timestamp.tsx => components/FormattedTimestamp/index.tsx} (51%) create mode 100644 src/renderer/src/components/NotificationButton/index.tsx create mode 100644 src/renderer/src/components/NotificationList/index.tsx create mode 100644 src/renderer/src/pages/secondary/NotificationListPage/index.tsx diff --git a/src/renderer/src/lib/timestamp.tsx b/src/renderer/src/components/FormattedTimestamp/index.tsx similarity index 51% rename from src/renderer/src/lib/timestamp.tsx rename to src/renderer/src/components/FormattedTimestamp/index.tsx index d3a9e8b..183bb04 100644 --- a/src/renderer/src/lib/timestamp.tsx +++ b/src/renderer/src/components/FormattedTimestamp/index.tsx @@ -1,30 +1,36 @@ import dayjs from 'dayjs' import { useTranslation } from 'react-i18next' -export function formatTimestamp(timestamp: number) { +export function FormattedTimestamp({ + timestamp, + short = false +}: { + timestamp: number + short?: boolean +}) { const { t } = useTranslation() const time = dayjs(timestamp * 1000) const now = dayjs() const diffMonth = now.diff(time, 'month') - if (diffMonth >= 1) { + if (diffMonth >= 2) { return t('date', { timestamp: time.valueOf() }) } const diffDay = now.diff(time, 'day') if (diffDay >= 1) { - return t('n days ago', { n: diffDay }) + return short ? t('n d', { n: diffDay }) : t('n days ago', { n: diffDay }) } const diffHour = now.diff(time, 'hour') if (diffHour >= 1) { - return t('n hours ago', { n: diffHour }) + return short ? t('n h', { n: diffHour }) : t('n hours ago', { n: diffHour }) } const diffMinute = now.diff(time, 'minute') if (diffMinute >= 1) { - return t('n minutes ago', { n: diffMinute }) + return short ? t('n m', { n: diffMinute }) : t('n minutes ago', { n: diffMinute }) } - return t('just now') + return short ? t('n s', { n: now.diff(time, 'second') }) : t('just now') } diff --git a/src/renderer/src/components/Note/index.tsx b/src/renderer/src/components/Note/index.tsx index 9fe760d..9d3d132 100644 --- a/src/renderer/src/components/Note/index.tsx +++ b/src/renderer/src/components/Note/index.tsx @@ -1,12 +1,12 @@ import { useSecondaryPage } from '@renderer/PageManager' import { toNote } from '@renderer/lib/link' -import { formatTimestamp } from '@renderer/lib/timestamp' import { Event } from 'nostr-tools' import Content from '../Content' +import { FormattedTimestamp } from '../FormattedTimestamp' import NoteStats from '../NoteStats' +import ParentNotePreview from '../ParentNotePreview' import UserAvatar from '../UserAvatar' import Username from '../Username' -import ParentNotePreview from '../ParentNotePreview' export default function Note({ event, @@ -38,7 +38,7 @@ export default function Note({ skeletonClassName={size === 'small' ? 'h-3' : 'h-4'} />
- {formatTimestamp(event.created_at)} +
diff --git a/src/renderer/src/components/NoteList/index.tsx b/src/renderer/src/components/NoteList/index.tsx index e2ef45f..2a93c73 100644 --- a/src/renderer/src/components/NoteList/index.tsx +++ b/src/renderer/src/components/NoteList/index.tsx @@ -56,9 +56,12 @@ export default function NoteList({ if (!areAlgoRelays) { events.sort((a, b) => b.created_at - a.created_at) } + events = events.slice(0, noteFilter.limit) if (events.length > 0) { setEvents((pre) => [...pre, ...events]) setUntil(events[events.length - 1].created_at - 1) + } else { + setHasMore(false) } if (areAlgoRelays) { setHasMore(false) @@ -111,7 +114,9 @@ export default function NoteList({ const loadMore = async () => { const events = await client.fetchEvents(relayUrls, { ...noteFilter, until }, true) - const sortedEvents = events.sort((a, b) => b.created_at - a.created_at) + const sortedEvents = events + .sort((a, b) => b.created_at - a.created_at) + .slice(0, noteFilter.limit) if (sortedEvents.length === 0) { setHasMore(false) return diff --git a/src/renderer/src/components/NotificationButton/index.tsx b/src/renderer/src/components/NotificationButton/index.tsx new file mode 100644 index 0000000..30d41c7 --- /dev/null +++ b/src/renderer/src/components/NotificationButton/index.tsx @@ -0,0 +1,39 @@ +import { Button } from '@renderer/components/ui/button' +import { toNotifications } from '@renderer/lib/link' +import { useSecondaryPage } from '@renderer/PageManager' +import { Bell } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +export default function NotificationButton({ + variant = 'titlebar' +}: { + variant?: 'sidebar' | 'titlebar' | 'small-screen-titlebar' +}) { + const { t } = useTranslation() + const { push } = useSecondaryPage() + + if (variant === 'sidebar') { + return ( + + ) + } + + return ( + + ) +} diff --git a/src/renderer/src/components/NotificationList/index.tsx b/src/renderer/src/components/NotificationList/index.tsx new file mode 100644 index 0000000..cf4ba9e --- /dev/null +++ b/src/renderer/src/components/NotificationList/index.tsx @@ -0,0 +1,205 @@ +import { useFetchEvent } from '@renderer/hooks' +import { toNote } from '@renderer/lib/link' +import { tagNameEquals } from '@renderer/lib/tag' +import { useSecondaryPage } from '@renderer/PageManager' +import { useNostr } from '@renderer/providers/NostrProvider' +import client from '@renderer/services/client.service' +import dayjs from 'dayjs' +import { Heart, MessageCircle, Repeat } from 'lucide-react' +import { Event, kinds, nip19, validateEvent } from 'nostr-tools' +import { useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { FormattedTimestamp } from '../FormattedTimestamp' +import UserAvatar from '../UserAvatar' + +const LIMIT = 50 + +export default function NotificationList() { + const { t } = useTranslation() + const { pubkey } = useNostr() + const [initialized, setInitialized] = useState(false) + const [notifications, setNotifications] = useState([]) + const [until, setUntil] = useState(dayjs().unix()) + const bottomRef = useRef(null) + const observer = useRef(null) + const [hasMore, setHasMore] = useState(true) + + useEffect(() => { + if (!pubkey) { + setHasMore(false) + return + } + + const init = async () => { + setHasMore(true) + const subCloser = await client.subscribeNotifications(pubkey, LIMIT, { + onNotifications: (events, isCache) => { + setNotifications(events) + setUntil(events.length ? events[events.length - 1].created_at - 1 : dayjs().unix()) + if (!isCache) { + setInitialized(true) + } + }, + onNew: (event) => { + setNotifications((oldEvents) => [event, ...oldEvents]) + } + }) + + return subCloser + } + + const promise = init() + return () => { + promise.then((closer) => closer?.()) + } + }, [pubkey]) + + useEffect(() => { + if (!initialized) return + + const options = { + root: null, + rootMargin: '10px', + threshold: 1 + } + + observer.current = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && hasMore) { + loadMore() + } + }, options) + + if (bottomRef.current) { + observer.current.observe(bottomRef.current) + } + + return () => { + if (observer.current && bottomRef.current) { + observer.current.unobserve(bottomRef.current) + } + } + }, [until, initialized, hasMore]) + + const loadMore = async () => { + if (!pubkey) return + const notifications = await client.fetchMoreNotifications(pubkey, until, LIMIT) + if (notifications.length === 0) { + setHasMore(false) + return + } + + if (notifications.length > 0) { + setNotifications((oldNotifications) => [...oldNotifications, ...notifications]) + } + + setUntil(notifications[notifications.length - 1].created_at - 1) + } + + return ( +
+ {notifications.map((notification, index) => ( + + ))} +
+ {hasMore ?
{t('loading...')}
: t('no more notifications')} +
+
+ ) +} + +function NotificationItem({ notification }: { notification: Event }) { + if (notification.kind === kinds.Reaction) { + return + } + if (notification.kind === kinds.ShortTextNote) { + return + } + if (notification.kind === kinds.Repost) { + return + } + return null +} + +function ReactionNotification({ notification }: { notification: Event }) { + const { push } = useSecondaryPage() + const bech32Id = useMemo(() => { + const eTag = notification.tags.findLast(tagNameEquals('e')) + const pTag = notification.tags.find(tagNameEquals('p')) + const eventId = eTag?.[1] + const author = pTag?.[1] + return eventId + ? nip19.neventEncode(author ? { id: eventId, author } : { id: eventId }) + : undefined + }, [notification.id]) + const { event } = useFetchEvent(bech32Id) + if (!event || !bech32Id || event.kind !== kinds.ShortTextNote) return null + + return ( +
push(toNote(bech32Id))} + > +
+ + + +
+
+ +
+
+ ) +} + +function ReplyNotification({ notification }: { notification: Event }) { + const { push } = useSecondaryPage() + return ( +
push(toNote(notification.id))} + > + + + +
+ +
+
+ ) +} + +function RepostNotification({ notification }: { notification: Event }) { + const { push } = useSecondaryPage() + const event = useMemo(() => { + try { + const event = JSON.parse(notification.content) as Event + const isValid = validateEvent(event) + if (!isValid) return null + client.addEventToCache(event) + return event + } catch { + return null + } + }, []) + if (!event) return null + + return ( +
push(toNote(event.id))} + > + + + +
+ +
+
+ ) +} + +function ContentPreview({ event }: { event?: Event }) { + if (!event || event.kind !== kinds.ShortTextNote) return null + + return
{event.content}
+} diff --git a/src/renderer/src/components/ReplyNote/index.tsx b/src/renderer/src/components/ReplyNote/index.tsx index a66d5a3..7a4f893 100644 --- a/src/renderer/src/components/ReplyNote/index.tsx +++ b/src/renderer/src/components/ReplyNote/index.tsx @@ -1,13 +1,13 @@ -import { formatTimestamp } from '@renderer/lib/timestamp' import { Event } from 'nostr-tools' import { useState } from 'react' +import { useTranslation } from 'react-i18next' import Content from '../Content' +import { FormattedTimestamp } from '../FormattedTimestamp' import LikeButton from '../NoteStats/LikeButton' import ParentNotePreview from '../ParentNotePreview' import PostDialog from '../PostDialog' import UserAvatar from '../UserAvatar' import Username from '../Username' -import { useTranslation } from 'react-i18next' export default function ReplyNote({ event, @@ -39,7 +39,9 @@ export default function ReplyNote({ )}
-
{formatTimestamp(event.created_at)}
+
+ +
setIsPostDialogOpen(true)} diff --git a/src/renderer/src/components/Sidebar/index.tsx b/src/renderer/src/components/Sidebar/index.tsx index a815163..c1c4c75 100644 --- a/src/renderer/src/components/Sidebar/index.tsx +++ b/src/renderer/src/components/Sidebar/index.tsx @@ -7,6 +7,7 @@ import { Info } from 'lucide-react' import { useTranslation } from 'react-i18next' import AboutInfoDialog from '../AboutInfoDialog' import AccountButton from '../AccountButton' +import NotificationButton from '../NotificationButton' import PostButton from '../PostButton' import RefreshButton from '../RefreshButton' import RelaySettingsButton from '../RelaySettingsButton' @@ -24,6 +25,7 @@ export default function PrimaryPageSidebar() {
+ {!IS_ELECTRON && ( diff --git a/src/renderer/src/i18n/en.ts b/src/renderer/src/i18n/en.ts index 783a8b5..b1339b6 100644 --- a/src/renderer/src/i18n/en.ts +++ b/src/renderer/src/i18n/en.ts @@ -12,9 +12,13 @@ export default { Following: 'Following', reposted: 'reposted', 'just now': 'just now', + 'n s': '{{n}}s', 'n minutes ago': '{{n}} minutes ago', + 'n m': '{{n}}m', 'n hours ago': '{{n}} hours ago', + 'n h': '{{n}}h', 'n days ago': '{{n}} days ago', + 'n d': '{{n}}d', date: '{{timestamp, date}}', Follow: 'Follow', Unfollow: 'Unfollow', @@ -72,6 +76,9 @@ export default { 'all users': 'all users', 'Display replies': 'Display replies', Notes: 'Notes', - 'Notes & Replies': 'Notes & Replies' + 'Notes & Replies': 'Notes & Replies', + notifications: 'notifications', + Notifications: 'Notifications', + 'no more notifications': 'no more notifications' } } diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts index d8229fc..b1e63be 100644 --- a/src/renderer/src/i18n/index.ts +++ b/src/renderer/src/i18n/index.ts @@ -21,11 +21,11 @@ i18n } }) -i18n.services.formatter?.add('date', (value, lng) => { +i18n.services.formatter?.add('date', (timestamp, lng) => { if (lng?.startsWith('zh')) { - return dayjs(value).format('YYYY-MM-DD') + return dayjs(timestamp).format('YYYY/MM/DD') } - return dayjs(value).format('MMM D, YYYY') + return dayjs(timestamp).format('MMM D, YYYY') }) export default i18n diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts index 21e82ef..d6bf085 100644 --- a/src/renderer/src/i18n/zh.ts +++ b/src/renderer/src/i18n/zh.ts @@ -12,9 +12,13 @@ export default { Following: '关注', reposted: '转发', 'just now': '刚刚', + 'n s': '{{n}}秒', 'n minutes ago': '{{n}} 分钟前', + 'n m': '{{n}}分', 'n hours ago': '{{n}} 小时前', + 'n h': '{{n}}时', 'n days ago': '{{n}} 天前', + 'n d': '{{n}}天', date: '{{timestamp, date}}', Follow: '关注', Unfollow: '取消关注', @@ -71,6 +75,9 @@ export default { 'all users': '所有用户', 'Display replies': '显示回复', Notes: '笔记', - 'Notes & Replies': '笔记 & 回复' + 'Notes & Replies': '笔记 & 回复', + notifications: '通知', + Notifications: '通知', + 'no more notifications': '到底了' } } diff --git a/src/renderer/src/layouts/PrimaryPageLayout/index.tsx b/src/renderer/src/layouts/PrimaryPageLayout/index.tsx index 058c7bb..b7eba7a 100644 --- a/src/renderer/src/layouts/PrimaryPageLayout/index.tsx +++ b/src/renderer/src/layouts/PrimaryPageLayout/index.tsx @@ -1,5 +1,6 @@ import Logo from '@renderer/assets/Logo' import AccountButton from '@renderer/components/AccountButton' +import NotificationButton from '@renderer/components/NotificationButton' import PostButton from '@renderer/components/PostButton' import RefreshButton from '@renderer/components/RefreshButton' import RelaySettingsButton from '@renderer/components/RelaySettingsButton' @@ -38,12 +39,13 @@ const PrimaryPageLayout = forwardRef(({ children }: { children?: React.ReactNode return } - if (diff > 50) { + if (diff > 20) { setVisible(false) - } else if (diff < -50) { + setLastScrollTop(scrollTop) + } else if (diff < -20) { setVisible(true) + setLastScrollTop(scrollTop) } - setLastScrollTop(scrollTop) } const scrollArea = scrollAreaRef.current @@ -94,6 +96,7 @@ function PrimaryPageTitlebar({ visible = true }: { visible?: boolean }) { +
@@ -110,6 +113,7 @@ function PrimaryPageTitlebar({ visible = true }: { visible?: boolean }) {
+
) diff --git a/src/renderer/src/layouts/SecondaryPageLayout/index.tsx b/src/renderer/src/layouts/SecondaryPageLayout/index.tsx index 5d51294..c60f343 100644 --- a/src/renderer/src/layouts/SecondaryPageLayout/index.tsx +++ b/src/renderer/src/layouts/SecondaryPageLayout/index.tsx @@ -33,12 +33,13 @@ export default function SecondaryPageLayout({ return } - if (diff > 50) { + if (diff > 20) { setVisible(false) - } else if (diff < -50) { + setLastScrollTop(scrollTop) + } else if (diff < -20) { setVisible(true) + setLastScrollTop(scrollTop) } - setLastScrollTop(scrollTop) } const scrollArea = scrollAreaRef.current diff --git a/src/renderer/src/lib/link.ts b/src/renderer/src/lib/link.ts index 720c0b0..5d7859c 100644 --- a/src/renderer/src/lib/link.ts +++ b/src/renderer/src/lib/link.ts @@ -25,6 +25,7 @@ export const toProfileList = ({ search }: { search?: string }) => { } export const toFollowingList = (pubkey: string) => `/users/${pubkey}/following` export const toRelaySettings = () => '/relay-settings' +export const toNotifications = () => '/notifications' export const toNoStrudelProfile = (id: string) => `https://nostrudel.ninja/#/u/${id}` export const toNoStrudelNote = (id: string) => `https://nostrudel.ninja/#/n/${id}` diff --git a/src/renderer/src/pages/secondary/NotificationListPage/index.tsx b/src/renderer/src/pages/secondary/NotificationListPage/index.tsx new file mode 100644 index 0000000..7015602 --- /dev/null +++ b/src/renderer/src/pages/secondary/NotificationListPage/index.tsx @@ -0,0 +1,15 @@ +import NotificationList from '@renderer/components/NotificationList' +import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout' +import { useTranslation } from 'react-i18next' + +export default function NotificationListPage() { + const { t } = useTranslation() + + return ( + +
+ +
+
+ ) +} diff --git a/src/renderer/src/providers/NostrProvider.tsx b/src/renderer/src/providers/NostrProvider.tsx index 3517afe..a18389a 100644 --- a/src/renderer/src/providers/NostrProvider.tsx +++ b/src/renderer/src/providers/NostrProvider.tsx @@ -103,6 +103,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { await window.api.nostr.logout() } setPubkey(null) + client.clearNotificationsCache() } const publish = async (draftEvent: TDraftEvent, additionalRelayUrls: string[] = []) => { diff --git a/src/renderer/src/routes.tsx b/src/renderer/src/routes.tsx index e90a7a2..00859f4 100644 --- a/src/renderer/src/routes.tsx +++ b/src/renderer/src/routes.tsx @@ -4,6 +4,7 @@ import FollowingListPage from './pages/secondary/FollowingListPage' import HomePage from './pages/secondary/HomePage' import NoteListPage from './pages/secondary/NoteListPage' import NotePage from './pages/secondary/NotePage' +import NotificationListPage from './pages/secondary/NotificationListPage' import ProfileListPage from './pages/secondary/ProfileListPage' import ProfilePage from './pages/secondary/ProfilePage' import RelaySettingsPage from './pages/secondary/RelaySettingsPage' @@ -15,7 +16,8 @@ const ROUTES = [ { path: '/users', element: }, { path: '/users/:id', element: }, { path: '/users/:id/following', element: }, - { path: '/relay-settings', element: } + { path: '/relay-settings', element: }, + { path: '/notifications', element: } ] export const routes = ROUTES.map(({ path, element }) => ({ diff --git a/src/renderer/src/services/client.service.ts b/src/renderer/src/services/client.service.ts index 72d471a..11f4702 100644 --- a/src/renderer/src/services/client.service.ts +++ b/src/renderer/src/services/client.service.ts @@ -41,6 +41,7 @@ class ClientService { private repliesCache = new LRUCache({ max: 1000 }) + private notificationsCache: [string, number][] = [] private profileCache = new LRUCache>({ max: 10000 }) private profileDataloader = new DataLoader( (ids) => Promise.all(ids.map((id) => this._fetchProfile(id))), @@ -211,9 +212,8 @@ class ClientService { ], { onevent(evt: NEvent) { - if (!isReplyNoteEvent(evt)) return - if (hasEosed) { + if (!isReplyNoteEvent(evt)) return onNew(evt) } else { events.push(evt) @@ -222,7 +222,10 @@ class ClientService { }, oneose() { hasEosed = true - const newReplies = events.sort((a, b) => a.created_at - b.created_at) + const newReplies = events + .sort((a, b) => a.created_at - b.created_at) + .slice(0, limit) + .filter(isReplyNoteEvent) replies = replies.concat(newReplies) // first fetch if (!since) { @@ -250,8 +253,87 @@ class ClientService { } } + async subscribeNotifications( + pubkey: string, + limit: number, + { + onNotifications, + onNew + }: { + onNotifications: (events: NEvent[], isCache: boolean) => void + onNew: (evt: NEvent) => void + } + ) { + let cachedNotifications: NEvent[] = [] + if (this.notificationsCache.length) { + cachedNotifications = ( + await Promise.all(this.notificationsCache.map(([id]) => this.eventCache.get(id))) + ).filter(Boolean) as NEvent[] + onNotifications(cachedNotifications, true) + } + const since = this.notificationsCache.length ? this.notificationsCache[0][1] + 1 : undefined + + const relayList = await this.fetchRelayList(pubkey) + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this + const events: NEvent[] = [] + let hasEosed = false + let count = 0 + const closer = this.pool.subscribeMany( + relayList.read.length >= 4 + ? relayList.read + : relayList.read.concat(this.defaultRelayUrls).slice(0, 4), + [ + { + kinds: [kinds.ShortTextNote, kinds.Repost, kinds.Reaction], + '#p': [pubkey], + limit, + since + } + ], + { + onevent(evt: NEvent) { + count++ + if (hasEosed) { + if (evt.pubkey === pubkey) return + onNew(evt) + } else { + events.push(evt) + } + that.eventDataLoader.prime(evt.id, Promise.resolve(evt)) + }, + oneose() { + hasEosed = true + const newNotifications = events + .sort((a, b) => b.created_at - a.created_at) + .slice(0, limit) + .filter((evt) => evt.pubkey !== pubkey) + if (count >= limit) { + that.notificationsCache = newNotifications.map( + (evt) => [evt.id, evt.created_at] as [string, number] + ) + onNotifications(newNotifications, false) + } else { + that.notificationsCache = [ + ...newNotifications.map((evt) => [evt.id, evt.created_at] as [string, number]), + ...that.notificationsCache + ] + onNotifications(newNotifications.concat(cachedNotifications), false) + } + } + } + ) + + return () => { + onNotifications = () => {} + onNew = () => {} + closer.close() + } + } + async fetchMoreReplies(relayUrls: string[], parentEventId: string, until: number, limit: number) { - const events = await this.pool.querySync(relayUrls, { + let events = await this.pool.querySync(relayUrls, { '#e': [parentEventId], kinds: [kinds.ShortTextNote], limit, @@ -260,7 +342,7 @@ class ClientService { events.forEach((evt) => { this.eventDataLoader.prime(evt.id, Promise.resolve(evt)) }) - events.sort((a, b) => a.created_at - b.created_at) + events = events.sort((a, b) => a.created_at - b.created_at).slice(0, limit) const replies = events.filter((evt) => isReplyNoteEvent(evt)) let cache = this.repliesCache.get(parentEventId) if (!cache) { @@ -282,6 +364,44 @@ class ClientService { return { replies, until: cache.until } } + async fetchMoreNotifications(pubkey: string, until: number, limit: number) { + const relayList = await this.fetchRelayList(pubkey) + const events = await this.pool.querySync( + relayList.read.length >= 4 + ? relayList.read + : relayList.read.concat(this.defaultRelayUrls).slice(0, 4), + { + kinds: [kinds.ShortTextNote, kinds.Repost, kinds.Reaction], + '#p': [pubkey], + limit, + until + } + ) + events.forEach((evt) => { + this.eventDataLoader.prime(evt.id, Promise.resolve(evt)) + }) + const notifications = events + .sort((a, b) => b.created_at - a.created_at) + .slice(0, limit) + .filter((evt) => evt.pubkey !== pubkey) + + const cacheLastCreatedAt = this.notificationsCache.length + ? this.notificationsCache[this.notificationsCache.length - 1][1] + : undefined + this.notificationsCache = this.notificationsCache.concat( + (cacheLastCreatedAt + ? notifications.filter((evt) => evt.created_at < cacheLastCreatedAt) + : notifications + ).map((evt) => [evt.id, evt.created_at] as [string, number]) + ) + + return notifications + } + + clearNotificationsCache() { + this.notificationsCache = [] + } + async fetchEvents(relayUrls: string[], filter: Filter, cache = false) { const events = await this.pool.querySync( relayUrls.length > 0 ? relayUrls : this.defaultRelayUrls,