diff --git a/CHANGELOG.md b/CHANGELOG.md index 557ccda7..4029bd95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - [x] Publish q tags - [x] Add sugggested relays based on follows - [x] Improve invite creation +- [x] Added docker image (@kornpow) # 0.4.3 diff --git a/src/app/views/Notifications.svelte b/src/app/views/Notifications.svelte index 45d458d9..69ab710e 100644 --- a/src/app/views/Notifications.svelte +++ b/src/app/views/Notifications.svelte @@ -80,13 +80,25 @@ loadGroupMessages() loadNotifications() + const unsubUnreadNotifications = unreadNotifications.subscribe(events => { + if (activeTab !== "Groups") { + markAsSeen(events) + } + }) + + const unsubUnreadGroupNotifications = unreadGroupNotifications.subscribe(events => { + if (activeTab === "Groups") { + markAsSeen(events) + } + }) + const scroller = createScroller(async () => { limit += 4 }) return () => { - markAsSeen($unreadNotifications) - markAsSeen($unreadGroupNotifications) + unsubUnreadNotifications() + unsubUnreadGroupNotifications() scroller.stop() } }) diff --git a/src/engine/events/commands.ts b/src/engine/events/commands.ts index 26a01f71..9877a675 100644 --- a/src/engine/events/commands.ts +++ b/src/engine/events/commands.ts @@ -1,70 +1,55 @@ -import {pluck, uniq, flatten} from "ramda" -import {chunk, batch, seconds} from "hurdak" +import {pluck} from "ramda" +import {chunk, seconds} from "hurdak" import {createEvent, now} from "paravel" import {generatePrivateKey} from "src/util/nostr" import {pubkey} from "src/engine/session/state" -import {signer, nip44, nip59} from "src/engine/session/derived" +import {signer, nip59} from "src/engine/session/derived" import {hints} from "src/engine/relays/utils" import {Publisher} from "src/engine/network/utils" import type {Event} from "./model" -import {seenIds} from "./state" +import {seen} from "./state" -const getExpirationTag = () => ["expiration", String(now() + seconds(90, "day"))] - -const createReadReceipt = ids => - createEvent(15, { - tags: [getExpirationTag(), ...ids.map(id => ["e", id])], - }) - -export const markAsSeenPublicly = batch(5000, async idChunks => { - for (const ids of chunk(500, uniq(flatten(idChunks)))) { - const event = await signer.get().signAsUser(createReadReceipt(ids)) - - if (event) { - Publisher.publish({event, relays: hints.WriteRelays().getUrls()}) - } - } -}) - -export const markAsSeenPrivately = batch(5000, async idChunks => { - for (const ids of chunk(500, uniq(flatten(idChunks)))) { - const template = createReadReceipt(ids) - - const rumor = await nip59.get().wrap(template, { - wrap: { - author: generatePrivateKey(), - recipient: pubkey.get(), - }, - }) - - rumor.wrap.tags.push(getExpirationTag()) - - Publisher.publish({ - event: rumor.wrap, - relays: hints.WriteRelays().getUrls(), - }) - } -}) - -export const markAsSeen = async (events: Event[], {visibility = "private"} = {}) => { +export const markAsSeen = async (events: Event[]) => { if (!signer.get().isEnabled() || events.length === 0) { return } const ids = pluck("id", events) - // Eagerly update seenIds to make the UX smooth - seenIds.update($seenIds => { + // Eagerly update to make the UX smooth + seen.mapStore.update($m => { for (const id of ids) { - $seenIds.add(id) + if (!$m.has(id)) { + $m.set(id, {id}) + } } - return $seenIds + return $m }) - if (visibility === "private" && nip44.get().isEnabled()) { - markAsSeenPrivately(ids) - } else { - markAsSeenPublicly(ids) + const notSynced = seen.get().filter(x => !x.published) + + if (notSynced.length > 100) { + const expirationTag = ["expiration", String(now() + seconds(90, "day"))] + + for (const ids of chunk(500, pluck("id", notSynced))) { + const template = createEvent(15, { + tags: [expirationTag, ...ids.map(id => ["e", id])], + }) + + const rumor = await nip59.get().wrap(template, { + wrap: { + author: generatePrivateKey(), + recipient: pubkey.get(), + }, + }) + + rumor.wrap.tags.push(expirationTag) + + Publisher.publish({ + event: rumor.wrap, + relays: hints.WriteRelays().getUrls(), + }) + } } } diff --git a/src/engine/events/derived.ts b/src/engine/events/derived.ts index 6c481536..daa44760 100644 --- a/src/engine/events/derived.ts +++ b/src/engine/events/derived.ts @@ -7,7 +7,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, seen, _events} from "./state" export const events = new DerivedCollection("id", [_events, deletes], ([$e, $d]) => $e.filter(e => !$d.has(e.id)), @@ -72,3 +72,5 @@ export const isEventMuted = derived([mutes, settings, pubkey], ([$mutes, $settin export const isDeleted = deletes.derived( $d => e => Boolean(getIdAndAddress(e).find(k => $d.has(k))), ) + +export const isSeen = seen.mapStore.derived($m => e => $m.has(e.id)) diff --git a/src/engine/events/model.ts b/src/engine/events/model.ts index 49a14c82..04f22bd7 100644 --- a/src/engine/events/model.ts +++ b/src/engine/events/model.ts @@ -11,3 +11,8 @@ export type Event = Omit & { export type Rumor = UnsignedEvent & { id: string } + +export type ReadReceipt = { + id: string + published?: number +} diff --git a/src/engine/events/projections.ts b/src/engine/events/projections.ts index c3b943a6..eeb498b8 100644 --- a/src/engine/events/projections.ts +++ b/src/engine/events/projections.ts @@ -65,7 +65,9 @@ projections.addHandler( seen.mapStore.update($m => { for (const e of chunk) { - $m.set(e.id, e) + for (const id of Tags.fromEvent(e).values("e").valueOf()) { + $m.set(id, {id, published: e.created_at}) + } } return $m diff --git a/src/engine/events/state.ts b/src/engine/events/state.ts index 7dc123e1..b880f28e 100644 --- a/src/engine/events/state.ts +++ b/src/engine/events/state.ts @@ -1,20 +1,6 @@ -import {Tags} from "paravel" import {Collection, Writable} from "src/engine/core/utils" -import type {Event} from "./model" +import type {Event, ReadReceipt} from "./model" export const _events = new Collection("id", 1000) -export const seen = new Collection("id", 1000) -export const seenIds = new Writable(new Set(), 1000) +export const seen = new Collection("id", 1000) export const deletes = new Writable(new Set(), 10000) - -seen.subscribe($seen => { - seenIds.update($seenIds => { - for (const e of $seen) { - for (const id of Tags.fromEvent(e).values("e").uniq().valueOf()) { - $seenIds.add(id) - } - } - - return $seenIds - }) -}) diff --git a/src/engine/index.ts b/src/engine/index.ts index 57e0d310..b8a2f972 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -54,11 +54,11 @@ const sessionsAdapter = { dump: identity, } -export const storage = new Storage(9, [ +export const storage = new Storage(10, [ new LocalStorageAdapter("pubkey", pubkey), new LocalStorageAdapter("sessions", sessions, sessionsAdapter), new LocalStorageAdapter("deletes2", deletes, setAdapter), - new IndexedDBAdapter("seen2", seen, 1000, sortBy(prop("created_at"))), + new IndexedDBAdapter("seen3", seen, 10000, 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"))), diff --git a/src/engine/network/utils/feed.ts b/src/engine/network/utils/feed.ts index 395e1deb..0939f3db 100644 --- a/src/engine/network/utils/feed.ts +++ b/src/engine/network/utils/feed.ts @@ -7,7 +7,7 @@ import {noteKinds, reactionKinds, repostKinds} from "src/util/nostr" import type {DisplayEvent} from "src/engine/notes/model" import type {Event} from "src/engine/events/model" import {sortEventsDesc, unwrapRepost} from "src/engine/events/utils" -import {isEventMuted, isDeleted} from "src/engine/events/derived" +import {isEventMuted, isDeleted, isSeen} from "src/engine/events/derived" import {writable} from "src/engine/core/utils" import type {Filter} from "../model" import {getIdFilters, guessFilterDelta} from "./filters" @@ -42,6 +42,7 @@ export class FeedLoader { ready: Promise isEventMuted = isEventMuted.get() isDeleted = isDeleted.get() + isSeen = isSeen.get() constructor(readonly opts: FeedOpts) { const urls = getUrls(opts.relays) @@ -97,6 +98,10 @@ export class FeedLoader { const strict = this.opts.filters.some(f => f["#a"]) return events.filter(e => { + if (this.isSeen(e)) { + return false + } + if (this.isDeleted(e)) { return false } diff --git a/src/engine/notifications/derived.ts b/src/engine/notifications/derived.ts index 8f46c655..9b248c9d 100644 --- a/src/engine/notifications/derived.ts +++ b/src/engine/notifications/derived.ts @@ -3,7 +3,7 @@ import {seconds} from "hurdak" import {now, Tags} from "paravel" import {isLike, reactionKinds, noteKinds, repostKinds} from "src/util/nostr" import {tryJson} from "src/util/misc" -import {seenIds} from "src/engine/events/state" +import {isSeen} from "src/engine/events/derived" import {unwrapRepost} from "src/engine/events/utils" import {events, isEventMuted} from "src/engine/events/derived" import {derived} from "src/engine/core/utils" @@ -40,16 +40,13 @@ export const notifications = derived( }, ) -export const unreadNotifications = derived( - [seenIds, notifications], - ([$seenIds, $notifications]) => { - const since = now() - seconds(30, "day") +export const unreadNotifications = derived([isSeen, notifications], ([$isSeen, $notifications]) => { + const since = now() - seconds(30, "day") - return $notifications.filter( - e => !reactionKinds.includes(e.kind) && e.created_at > since && !$seenIds.has(e.id), - ) - }, -) + return $notifications.filter( + e => !reactionKinds.includes(e.kind) && e.created_at > since && !$isSeen(e), + ) +}) export const groupNotifications = derived( [session, events, groupRequests, groupAlerts, groupAdminKeys], @@ -88,11 +85,11 @@ export const groupNotifications = derived( }) export const unreadGroupNotifications = derived( - [seenIds, groupNotifications], - ([$seenIds, $groupNotifications]) => { + [isSeen, groupNotifications], + ([$isSeen, $groupNotifications]) => { const since = now() - seconds(30, "day") - return $groupNotifications.filter(e => e.created_at > since && !$seenIds.has(e.id)) + return $groupNotifications.filter(e => e.created_at > since && !$isSeen(e)) }, )