Clean up read receipts for notifications

This commit is contained in:
Jon Staab 2024-01-26 15:48:35 -08:00
parent e262d65499
commit 21565482c8
17 changed files with 197 additions and 171 deletions

2
.env
View File

@ -1,7 +1,7 @@
VITE_THEME="transparent:transparent,black:#0f0f0e,white:#FFFFFF,accent:#EB5E28,accent-l:#FB652C,gray-dark:#8D8581,gray-light:#A8A5A4,danger:#ff0000,warning:#ebd112,success:#37ab51,input:#FAF6F1,input-hover:#F2EBE1"
VITE_DARK_THEME="black:#0f0f0e,white:#FFFFFF,transparent:transparent,accent:#fc560e,accent-l:#fe7d39,accent-d:#ed3f09,warm:#F7E9E2,warm-l:#fcf7f4,warm-d:#b9a69e,dark:#2C2C2C,dark-d:#232323,dark-l:#323131,mid:#474747,light:#5B5B5B,lighter:#888888,lightest:#d1d1d1,cocoa:#3E3A38,cocoa-l:#756A65,cocoa-d:#282524,danger:#ce0b0b"
VITE_LIGHT_THEME="black:#0f0f0e,white:#FFFFFF,transparent:transparent,accent:#fc560e,accent-l:#fe7d39,accent-d:#ed3f09,warm:#372113,warm-l:#372113,warm-d:#372113,dark:#ffffff,dark-d:#ffffff,dark-l:#ffffff,mid:#cbc7c2,light:#a49c90,lighter:#69635d,lightest:#3e342c,cocoa:#fafafa,cocoa-l:#fafafa,cocoa-d:#fafafa,danger:#ce0b0b"
VITE_DVM_RELAYS=wss://relay.damus.io,wss://bucket.coracle.social,wss://offchain.pub
VITE_DVM_RELAYS=wss://relay.damus.io,wss://offchain.pub,wss://relay.f7z.io,wss://nos.lol,wss://relay.nostr.net,wss://relay.nostr.band,wss://bucket.coracle.social
VITE_DEFAULT_RELAYS=wss://purplepag.es,wss://relay.damus.io,wss://relay.nostr.band,wss://relayable.org,wss://nostr.wine
VITE_DEFAULT_FOLLOWS=5c508c34f58866ec7341aaf10cc1af52e9232bb9f859c8103ca5ecf2aa93bf78,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,958b754a1d3de5b5eca0fe31d2d555f451325f8498a83da1997b7fcd5c39e88c,fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93,7411d7394831af2c5ea2033fffea93b30e1a68655e8cf962845ce0b0d1e62eb3,88d530801d60d63afe1849926a1aa2a00d322ad98f0f8be53c6ce43d4ddd274f,eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f
VITE_NIP96_URLS=https://nostr.build,https://nostrcheck.me,https://sove.rent,https://void.cat

View File

@ -2,7 +2,7 @@
import {matchFilters} from "paravel"
import {reject, whereEq, uniqBy, prop} from "ramda"
import {onMount, onDestroy} from "svelte"
import {quantify, batch} from "hurdak"
import {quantify, ensurePlural, batch} from "hurdak"
import {Tags} from "paravel"
import {fly} from "src/util/transition"
import {
@ -116,8 +116,8 @@
ctx = reject(whereEq({id: e.id}), ctx)
}
const addToContext = e => {
ctx = ctx.concat({seen_on: [], ...e})
const addToContext = events => {
ctx = ctx.concat(ensurePlural(events).map(e => ({seen_on: [], ...e})))
}
$: tags = Tags.from(event)

View File

@ -1,11 +1,9 @@
<script lang="ts">
import {find, assoc} from "ramda"
import {find} from "ramda"
import {onMount} from "svelte"
import {now, createEvent} from "paravel"
import {chunk, seconds} from "hurdak"
import {debounce} from "throttle-debounce"
import {debounce} from 'throttle-debounce'
import {createScroller, formatTimestampAsDate} from "src/util/misc"
import {noteKinds, generatePrivateKey, reactionKinds} from "src/util/nostr"
import {noteKinds, reactionKinds} from "src/util/nostr"
import Tabs from "src/partials/Tabs.svelte"
import Content from "src/partials/Content.svelte"
import GroupAlert from "src/app/shared/GroupAlert.svelte"
@ -16,23 +14,17 @@
import {router} from "src/app/router"
import type {Event} from "src/engine"
import {
nip44,
nip59,
signer,
session,
markAsSeen,
notifications,
otherNotifications,
groupNotifications,
loadNotifications,
unreadNotifications,
updateCurrentSession,
publish,
unreadOtherNotifications,
} from "src/engine"
const tabs = ["Mentions & Replies", "Reactions", "Other"]
const lastSynced = $session?.notifications_last_synced || 0
const throttledNotifications = notifications.throttle(300)
const setActiveTab = tab => router.at("notifications").at(tab).push()
@ -63,47 +55,19 @@
find((e: Event) => reactionKinds.includes(e.kind), n.interactions),
)
$: uncheckedOtherNotifications = $otherNotifications.filter(n => n.created_at > lastSynced)
document.title = "Notifications"
onMount(() => {
loadNotifications()
const unsubAll = throttledNotifications.subscribe(() => {
updateCurrentSession(assoc("notifications_last_synced", now()))
})
const unsubUnread = unreadNotifications.subscribe(
debounce(3000, async $unreadNotifications => {
if ($signer.canSign) {
for (const events of chunk(500, $unreadNotifications)) {
const template = createEvent(15, {
tags: [["expiration", now() + seconds(90, "day")], ...events.map(e => ["e", e.id])],
})
publish(
await (!$nip44.isEnabled()
? $signer.signAsUser(template)
: $nip59.wrap(template, {
wrap: {
author: generatePrivateKey(),
recipient: $session.pubkey,
},
})),
)
}
}
}),
)
const unsub = unreadNotifications.subscribe(debounce(1000, markAsSeen))
const scroller = createScroller(async () => {
limit += 4
})
return () => {
unsubAll()
unsubUnread()
unsub()
scroller.stop()
}
})
@ -112,9 +76,9 @@
<Tabs {tabs} {activeTab} {setActiveTab}>
<div slot="tab" let:tab class="flex gap-2">
<div>{tab}</div>
{#if tab === tabs[2] && uncheckedOtherNotifications.length > 0}
{#if tab === tabs[2] && $unreadOtherNotifications.length > 0}
<div class="h-6 rounded-full bg-mid px-2">
{uncheckedOtherNotifications.length}
{$unreadOtherNotifications.length}
</div>
{/if}
</div>

View File

@ -0,0 +1,56 @@
import {chunk, seconds} from 'hurdak'
import {createEvent, now} from 'paravel'
import {generatePrivateKey} from "src/util/nostr"
import {session, signer, nip04, nip44, nip59} from 'src/engine/session/derived'
import {getUserRelayUrls} from 'src/engine/relays/utils'
import {Publisher} from 'src/engine/network/utils'
export const markAsSeen = async (allEvents: Event[], {visibility = 'private'} = {}) => {
if (!signer.get().isEnabled()) {
return
}
const pubs = []
const events = []
const {pubkey} = session.get()
const relays = getUserRelayUrls('write')
console.log('markAsSeen', allEvents)
for (const batch of chunk(500, allEvents)) {
const template = createEvent(15, {
tags: [["expiration", now() + seconds(90, "day")], ...batch.map(e => ["e", e.id])],
})
if (visibility === 'private' && nip44.get().isEnabled()) {
const rumor = await nip59.get().wrap(template, {
wrap: {
author: generatePrivateKey(),
recipient: pubkey,
},
})
events.push(rumor)
pubs.push(Publisher.publish({event: rumor.wrap, relays}))
} else if (visibility === 'private' && nip04.get().isEnabled()) {
const rumor = await nip59.get().wrap(template, {
wrap: {
kind: 1060,
algo: 'nip04',
author: generatePrivateKey(),
recipient: pubkey,
},
})
events.push(rumor)
pubs.push(Publisher.publish({event: rumor.wrap, relays}))
} else {
const event = await signer.get().signAsUser(template)
events.push(event)
pubs.push(Publisher.publish({event: event, relays}))
}
}
return {pubs, events}
}

View File

@ -8,7 +8,7 @@ import {getWotScore} from "src/engine/people/utils"
import {mutes, follows} from "src/engine/people/derived"
import {deriveIsGroupMember} from "src/engine/groups/utils"
import type {Event} from "./model"
import {deletes, _events} from "./state"
import {deletes, _events, seen} from "./state"
export const events = new DerivedCollection<Event>("id", [_events, deletes], ([$e, $d]) =>
$e.filter(e => !$d.has(e.id)),
@ -20,6 +20,8 @@ export const userEvents = new DerivedCollection<Event>("id", [events, pubkey], (
export const eventsByKind = events.derived(groupBy((e: Event) => String(e.kind)))
export const seenIds = seen.derived($seen => new Set(Tags.from($seen).type("e").values().all()))
export const isEventMuted = derived([mutes, settings, pubkey], ([$mutes, $settings, $pubkey]) => {
const words = $settings.muted_words
const minWot = $settings.min_wot_score

View File

@ -3,4 +3,5 @@ export * from "./state"
export * from "./utils"
export * from "./derived"
export * from "./requests"
export * from "./commands"
export * from "./projections"

View File

@ -1,11 +1,12 @@
import {max, pluck} from "ramda"
import {batch} from "hurdak"
import {batch, updateIn} from "hurdak"
import {Tags} from "paravel"
import {projections} from "src/engine/core/projections"
import type {Event} from "src/engine/events/model"
import {sessions} from "src/engine/session/state"
import {updateSession} from "src/engine/session/commands"
import {getNip04, getNip44, getNip59} from "src/engine/session/utils"
import {_events, deletes, seen, deletesLastUpdated, seenLastUpdated} from "./state"
import {_events, deletes, seen} from "./state"
const getSession = pubkey => sessions.get()[pubkey]
@ -25,7 +26,16 @@ projections.addHandler(
batch(500, (chunk: Event[]) => {
const values = Tags.from(chunk).type(["a", "e"]).values().all()
deletesLastUpdated.update(ts => max(ts, pluck("created_at", chunk).reduce(max)))
for (const pubkey of new Set(pluck("pubkey", chunk))) {
updateSession(
pubkey,
updateIn("deletes_last_synced", t =>
pluck("created_at", chunk)
.concat(t || 0)
.reduce(max, 0),
),
)
}
deletes.update($deletes => {
values.forEach(v => $deletes.add(v))
@ -38,42 +48,40 @@ projections.addHandler(
projections.addHandler(
15,
batch(500, (chunk: Event[]) => {
const values = Tags.from(chunk).type("e").values().all()
for (const pubkey of new Set(pluck("pubkey", chunk))) {
updateSession(
pubkey,
updateIn("seen_last_synced", t =>
pluck("created_at", chunk)
.concat(t || 0)
.reduce(max, 0),
),
)
}
seenLastUpdated.update(ts => max(ts, pluck("created_at", chunk).reduce(max)))
seen.mapStore.update($m => {
for (const e of chunk) {
$m.set(e.id, e)
}
seen.update($seen => {
values.forEach(v => $seen.add(v))
return $seen
return $m
})
}),
)
projections.addHandler(1059, wrap => {
const handleWrappedEvent = getEncryption => wrap => {
const session = getSession(Tags.from(wrap).pubkeys().first())
if (!session) {
return
}
if (getNip44(session).isEnabled()) {
if (getEncryption(session).isEnabled()) {
getNip59(session).withUnwrappedEvent(wrap, session.privkey, rumor => {
projections.push(rumor)
})
}
})
}
projections.addHandler(1060, wrap => {
const session = getSession(Tags.from(wrap).pubkeys().first())
if (!session) {
return
}
if (getNip04(session).isEnabled()) {
getNip59(session).withUnwrappedEvent(wrap, session.privkey, rumor => {
projections.push(rumor)
})
}
})
projections.addHandler(1059, handleWrappedEvent(getNip44))
projections.addHandler(1060, handleWrappedEvent(getNip04))

View File

@ -1,36 +1,36 @@
import {prop} from "ramda"
import {seconds} from 'hurdak'
import {sessions} from "src/engine/session/state"
import {seconds} from "hurdak"
import {session, nip44} from "src/engine/session/derived"
import {getUserHints} from "src/engine/relays/utils"
import {load} from "src/engine/network/utils"
import {deletesLastUpdated, seenLastUpdated, giftWrapLastFetched} from "./state"
export const loadDeletes = () => {
const since = Math.max(0, deletesLastUpdated.get() - seconds(6, 'hour'))
const authors = Object.values(sessions.get()).map(prop("pubkey"))
const {pubkey, deletes_last_synced = 0} = session.get()
const since = Math.max(0, deletes_last_synced - seconds(6, "hour"))
return load({
relays: getUserHints("write"),
filters: [{kinds: [5], authors, since}],
filters: [{kinds: [5], authors: [pubkey], since}],
})
}
export const loadSeen = () => {
const since = Math.max(0, seenLastUpdated.get() - seconds(6, 'hour'))
const authors = Object.values(sessions.get()).map(prop("pubkey"))
const {pubkey, deletes_last_synced = 0} = session.get()
const since = Math.max(0, deletes_last_synced - seconds(6, "hour"))
return load({
relays: getUserHints("write"),
filters: [{kinds: [15], authors, since}],
filters: [{kinds: [15], authors: [pubkey], since}],
})
}
export const loadGiftWrap = () => {
const since = Math.max(0, giftWrapLastFetched.get() - seconds(6, 'hour'))
const authors = Object.values(sessions.get()).filter(prop('privkey')).map(prop("pubkey"))
if (nip44.get().isEnabled()) {
const {pubkey, nip59_messages_last_synced = 0} = session.get()
const since = Math.max(0, nip59_messages_last_synced - seconds(6, "hour"))
return load({
relays: getUserHints(),
filters: [{kinds: [1059], authors, since}],
})
return load({
relays: getUserHints(),
filters: [{kinds: [1059, 1060], authors: [pubkey], since}],
})
}
}

View File

@ -1,9 +1,6 @@
import {Collection, Writable, writable} from "src/engine/core/utils"
import {Collection, Writable} from "src/engine/core/utils"
import type {Event} from "./model"
export const _events = new Collection<Event>("id", 1000)
export const seen = new Writable(new Set(), 10000)
export const seenLastUpdated = writable(0)
export const seen = new Collection<Event>("id", 1000)
export const deletes = new Writable(new Set(), 10000)
export const deletesLastUpdated = writable(0)
export const giftWrapLastFetched = writable(0)

View File

@ -6,7 +6,7 @@ import {relays} from "./relays"
import {groups, groupSharedKeys, groupAdminKeys, groupRequests, groupAlerts} from "./groups"
import {_labels} from "./labels"
import {topics} from "./topics"
import {deletes, seen, _events, seenLastUpdated, deletesLastUpdated, giftWrapLastFetched} from "./events"
import {deletes, seen, _events} from "./events"
import {pubkey, sessions} from "./session"
import {channels} from "./channels"
@ -34,14 +34,11 @@ const setAdapter = {
load: a => new Set(a || []),
}
export const storage = new Storage(8, [
export const storage = new Storage(9, [
new LocalStorageAdapter("pubkey", pubkey),
new LocalStorageAdapter("sessions", sessions),
new LocalStorageAdapter("deletes2", deletes, setAdapter),
new LocalStorageAdapter("seen", seen, setAdapter),
new LocalStorageAdapter("seenLastUpdated", seenLastUpdated),
new LocalStorageAdapter("deletesLastUpdated2", deletesLastUpdated),
new LocalStorageAdapter("giftWrapLastFetched", giftWrapLastFetched),
new IndexedDBAdapter("seen2", seen, 1000, sortBy(prop("created_at"))),
new IndexedDBAdapter("events", _events, 10000, sortByPubkeyWhitelist(prop("created_at"))),
new IndexedDBAdapter("labels", _labels, 1000, sortBy(prop("created_at"))),
new IndexedDBAdapter("topics", topics, 1000, sortBy(prop("last_seen"))),

View File

@ -1,7 +1,8 @@
import {now} from "paravel"
import {seconds} from "hurdak"
import {generatePrivateKey} from "src/util/nostr"
import {getUserHints} from "src/engine/relays/utils"
import {env} from "src/engine/session/state"
import {mergeHints, getUserHints} from "src/engine/relays/utils"
import type {Event} from "src/engine/events/model"
import {createAndPublish} from "./publish"
import {subscribe} from "./subscribe"
@ -9,6 +10,7 @@ import {subscribe} from "./subscribe"
export type DVMRequestOpts = {
kind: number
input: any
inputOpts?: string[]
tags?: string[][]
relays?: string[]
timeout?: number
@ -18,13 +20,14 @@ export type DVMRequestOpts = {
export const dvmRequest = async ({
kind,
input,
inputOpts = [],
tags = [],
timeout = 30_000,
relays = null,
onProgress = null,
}: DVMRequestOpts): Promise<Event> => {
if (!relays) {
relays = getUserHints()
relays = mergeHints([env.get().DVM_RELAYS, getUserHints()])
}
if (typeof input !== "string") {
@ -35,7 +38,7 @@ export const dvmRequest = async ({
relays,
sk: generatePrivateKey(),
tags: tags.concat([
["i", input],
["i", input, ...inputOpts],
["expiration", String(now() + seconds(1, "hour"))],
]),
})

View File

@ -1,4 +1,4 @@
import {partition, prop, uniqBy, identity, without, any, assoc} from "ramda"
import {partition, prop, uniqBy, identity, without, assoc} from "ramda"
import {ensurePlural, doPipe, batch} from "hurdak"
import {now, hasValidSignature, Tags} from "paravel"
import {race, tryJson} from "src/util/misc"
@ -54,9 +54,10 @@ export class FeedLoader {
constructor(readonly opts: FeedOpts) {
const urls = getUrls(opts.relays)
const filters = ensurePlural(opts.filters)
// No point in subscribing if we have an end date
if (opts.shouldListen && !any(prop("until"), ensurePlural(opts.filters) as any[])) {
if (opts.shouldListen && !filters.some(prop("until"))) {
this.addSubs([
subscribe({
relays: urls,

View File

@ -15,7 +15,7 @@ export type LoadPeopleOpts = {
export const attemptedPubkeys = new Map()
export const getStalePubkeys = (pubkeys: string[]) => {
export const getStalePubkeys = (pubkeys: Iterable<string>) => {
const stale = new Set<string>()
const since = now() - seconds(3, "hour")
@ -47,10 +47,10 @@ export const getStalePubkeys = (pubkeys: string[]) => {
}
export const loadPubkeys = async (
rawPubkeys: string[],
rawPubkeys: Iterable<string>,
{relays, force, kinds = personKinds}: LoadPeopleOpts = {},
) => {
const pubkeys = force ? uniq(rawPubkeys) : getStalePubkeys(rawPubkeys)
const pubkeys = force ? uniq(Array.from(rawPubkeys)) : getStalePubkeys(rawPubkeys)
if (pubkeys.length === 0) {
return

View File

@ -1,14 +1,12 @@
import {prop, apply, concat, assoc, max, sortBy} from "ramda"
import {apply, concat, assoc, max, sortBy} from "ramda"
import {seconds} from "hurdak"
import {now, Tags} from "paravel"
import {reactionKinds, getParentId} from "src/util/nostr"
import {reactionKinds, isGiftWrap, getParentId} from "src/util/nostr"
import {tryJson} from "src/util/misc"
import {seen} from "src/engine/events/state"
import {events, isEventMuted} from "src/engine/events/derived"
import {events, seenIds, isEventMuted} from "src/engine/events/derived"
import {derived} from "src/engine/core/utils"
import {groupRequests, groupAdminKeys, groupAlerts} from "src/engine/groups/state"
import {pubkey} from "src/engine/session/state"
import {session} from "src/engine/session/derived"
import {userEvents} from "src/engine/events/derived"
export const notifications = derived(
@ -21,7 +19,7 @@ export const notifications = derived(
const $isEventMuted = isEventMuted.get()
return $events.filter(e => {
if (e.pubkey === $pubkey || $isEventMuted(e)) {
if (e.pubkey === $pubkey || $isEventMuted(e) || isGiftWrap(e)) {
return false
}
@ -30,13 +28,16 @@ export const notifications = derived(
},
)
export const unreadNotifications = derived([seen, notifications], ([$seen, $notifications]) => {
const since = now() - seconds(30, "day")
export const unreadNotifications = derived(
[seenIds, notifications],
([$seenIds, $notifications]) => {
const since = now() - seconds(30, "day")
return $notifications.filter(
e => !reactionKinds.includes(e.kind) && e.created_at > since && !$seen.has(e.id),
)
})
return $notifications.filter(
e => !reactionKinds.includes(e.kind) && e.created_at > since && !$seenIds.has(e.id),
)
},
)
export const otherNotifications = derived(
[groupRequests, groupAlerts, groupAdminKeys],
@ -54,11 +55,11 @@ export const otherNotifications = derived(
)
export const unreadOtherNotifications = derived(
[seen, otherNotifications],
([$seen, $otherNotifications]) => {
[seenIds, otherNotifications],
([$seenIds, $otherNotifications]) => {
const since = now() - seconds(30, "day")
return $otherNotifications.filter(e => e.created_at > since && !$seen.has(e.id))
return $otherNotifications.filter(e => e.created_at > since && !$seenIds.has(e.id))
},
)
@ -67,15 +68,7 @@ export const unreadCombinedNotifications = derived(
apply(concat),
)
export const hasNewNotifications = derived(
[session, unreadCombinedNotifications],
([$session, $unreadCombinedNotifications]) => {
return $unreadCombinedNotifications.length > 0
const maxCreatedAt = $unreadCombinedNotifications.map(prop("created_at")).reduce(max, 0)
return maxCreatedAt > ($session?.notifications_last_synced || 0)
},
)
export const hasNewNotifications = unreadCombinedNotifications.derived($n => $n.length > 0)
export const groupNotifications = ($notifications, kinds) => {
const $userEvents = userEvents.mapStore.get()

View File

@ -1,10 +1,12 @@
import {now, Tags} from "paravel"
import {seconds, batch, doPipe} from "hurdak"
import {pluck, identity, max, slice, filter, without, sortBy} from "ramda"
import {seconds, updateIn, batch, doPipe} from "hurdak"
import {pluck, max, slice, filter, without, sortBy} from "ramda"
import {noteKinds, reactionKinds, getParentId} from "src/util/nostr"
import type {Event} from "src/engine/events/model"
import type {Filter} from "src/engine/network/model"
import {env, sessions} from "src/engine/session/state"
import {env} from "src/engine/session/state"
import {session} from "src/engine/session/derived"
import {updateSession} from "src/engine/session/commands"
import {_events} from "src/engine/events/state"
import {events, isEventMuted} from "src/engine/events/derived"
import {mergeHints, getPubkeyHints, getParentHints} from "src/engine/relays/utils"
@ -21,8 +23,20 @@ const onNotificationEvent = batch(300, (chunk: Event[]) => {
const $isEventMuted = isEventMuted.get()
const events = chunk.filter(e => kinds.includes(e.kind) && !$isEventMuted(e))
const eventsWithParent = chunk.filter(getParentId)
const pubkeys = new Set(pluck("pubkey", events))
loadPubkeys(pluck("pubkey", events))
for (const pubkey of pubkeys) {
updateSession(
pubkey,
updateIn("notifications_last_synced", t =>
pluck("created_at", events)
.concat(t || 0)
.reduce(max, 0),
),
)
}
loadPubkeys(pubkeys)
load({
relays: mergeHints(eventsWithParent.map(getParentHints)),
@ -42,58 +56,45 @@ const onNotificationEvent = batch(300, (chunk: Event[]) => {
export const getNotificationKinds = () =>
without(env.get().ENABLE_ZAPS ? [] : [9735], [...noteKinds, ...reactionKinds, 1059, 1060, 4])
const getEventIds = (pubkey: string) =>
doPipe(events.get(), [
filter((e: Event) => noteKinds.includes(e.kind) && e.pubkey === pubkey),
sortBy((e: Event) => -e.created_at),
slice(0, 256),
pluck("id"),
])
export const loadNotifications = () => {
const kinds = getNotificationKinds()
const pubkeys = Object.keys(sessions.get())
const cutoff = now() - seconds(30, "day")
const $sessions = Object.values(sessions.get())
const lastChecked = pluck("notifications_last_synced", $sessions).filter(identity).reduce(max, 0)
const since = Math.max(cutoff, lastChecked - seconds(1, "day"))
const eventIds = pluck(
"id",
sortBy(
e => -e.created_at,
events
.get()
.filter(
e =>
!reactionKinds.includes(e.kind) && e.created_at > cutoff && pubkeys.includes(e.pubkey),
),
).slice(0, 256),
)
const {pubkey, notifications_last_synced = 0} = session.get()
const since = Math.max(cutoff, notifications_last_synced - seconds(6, "hour"))
const eventIds = getEventIds(pubkey)
const filters = [
{kinds, "#p": pubkeys, since},
{kinds, "#p": [pubkey], since},
{kinds, "#e": eventIds, since},
{kinds, authors: pubkeys, since},
{kinds, authors: [pubkey], since},
]
return subscribe({
filters,
timeout: 15000,
skipCache: true,
relays: mergeHints(pubkeys.map(pk => getPubkeyHints(pk, "read"))),
relays: getPubkeyHints(pubkey, "read"),
onEvent: onNotificationEvent,
})
}
export const listenForNotifications = async () => {
const pubkeys = Object.keys(sessions.get())
const relays = mergeHints(pubkeys.map(pk => getPubkeyHints(pk, "read")))
const eventIds: string[] = doPipe(events.get(), [
filter((e: Event) => noteKinds.includes(e.kind)),
sortBy((e: Event) => -e.created_at),
slice(0, 256),
pluck("id"),
])
const {pubkey} = session.get()
const eventIds = getEventIds(pubkey)
const filters: Filter[] = [
// Messages/groups
{kinds: [4, 1059, 1060], "#p": pubkeys, limit: 1},
{kinds: [4, 1059, 1060], "#p": [pubkey], limit: 1},
// Mentions
{kinds: noteKinds, "#p": pubkeys, limit: 1},
{kinds: noteKinds, "#p": [pubkey], limit: 1},
]
// Replies
@ -104,9 +105,9 @@ export const listenForNotifications = async () => {
// Only grab one event from each category/relay so we have enough to show
// the notification badges, but load the details lazily
subscribePersistent({
relays,
filters,
skipCache: true,
relays: getPubkeyHints(pubkey, "read"),
onEvent: onNotificationEvent,
})
}

View File

@ -52,7 +52,7 @@ export const publishSettings = async (updates: Record<string, any>) => {
})
}
export const updateSession = (k, f) => sessions.update($s => ({...$s, [k]: f($s[k])}))
export const updateSession = (k, f) => sessions.update($s => ($s[k] ? {...$s, [k]: f($s[k])} : $s))
export const updateCurrentSession = f => {
const $pubkey = pubkey.get()

View File

@ -17,8 +17,11 @@ export type Session = {
bunkerRelay?: string
settings?: Record<string, any>
settings_updated_at?: number
seen_last_synced?: number
groups_last_synced?: number
deletes_last_synced?: number
notifications_last_synced?: number
nip24_messages_last_synced?: number
nip59_messages_last_synced?: number
groups?: Record<string, GroupStatus>
}