mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 00:10:52 +00:00
Clean up read receipts for notifications
This commit is contained in:
parent
e262d65499
commit
21565482c8
2
.env
2
.env
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
56
src/engine/events/commands.ts
Normal file
56
src/engine/events/commands.ts
Normal 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}
|
||||
}
|
@ -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
|
||||
|
@ -3,4 +3,5 @@ export * from "./state"
|
||||
export * from "./utils"
|
||||
export * from "./derived"
|
||||
export * from "./requests"
|
||||
export * from "./commands"
|
||||
export * from "./projections"
|
||||
|
@ -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))
|
||||
|
@ -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}],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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"))),
|
||||
|
@ -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"))],
|
||||
]),
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user