Skip to content

Commit

Permalink
Use local read receipts for notifications (#504)
Browse files Browse the repository at this point in the history
* local read receipts for notifications

* local read receipts for channels

* - fix read notification infinite loop
- move notifications.ts to /engine/

* clean notifications import

* - specific notifications prefix for channels
- removed unused notification's subscriptions

* path based seen check
  • Loading branch information
ticruz38 authored and Jon Staab committed Jan 15, 2025
1 parent 934b35c commit 8ff7d5e
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 299 deletions.
2 changes: 1 addition & 1 deletion src/app/Nav.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import PersonBadge from "src/app/shared/PersonBadge.svelte"
import {menuIsOpen, searchTerm} from "src/app/state"
import {router} from "src/app/util/router"
import {hasNewNotifications, hasNewMessages} from "src/engine"
import {hasNewMessages, hasNewNotifications} from "src/engine"
let innerWidth = 0
let searching = false
Expand Down
8 changes: 5 additions & 3 deletions src/app/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
env,
load,
loadPubkeys,
loadSeen,
loadDeletes,
loadHandlers,
loadMessages,
Expand All @@ -28,6 +27,7 @@ import {
listenForNotifications,
userFeedFavorites,
getSetting,
setChecked,
} from "src/engine"

export const drafts = new Map<string, string>()
Expand Down Expand Up @@ -91,7 +91,6 @@ export const loadUserData = async (hints: string[] = []) => {
loadPubkeys([env.PLATFORM_PUBKEY])

// Load anything they might need to be notified about
loadSeen()
loadMessages()
loadNotifications()
loadFeedsAndLists()
Expand All @@ -102,4 +101,7 @@ export const loadUserData = async (hints: string[] = []) => {
listenForNotifications()
}

export const boot = () => router.at("login/connect").open({noEscape: true, mini: true})
export const boot = () => {
router.at("login/connect").open({noEscape: true, mini: true})
setChecked("*")
}
6 changes: 3 additions & 3 deletions src/app/views/ChannelsDetail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import PersonCircles from "src/app/shared/PersonCircles.svelte"
import PersonAbout from "src/app/shared/PersonAbout.svelte"
import {router} from "src/app/util/router"
import {markChannelRead, getChannelIdFromEvent, listenForMessages} from "src/engine"
import Popover from "src/partials/Popover.svelte"
import {getChannelIdFromEvent, listenForMessages, setChecked} from "src/engine"
export let pubkeys
export let channelId
Expand All @@ -34,7 +34,7 @@
const sub = listenForMessages(pubkeys)
isAccepted = $messages.some(m => m.pubkey === $session.pubkey)
markChannelRead(channelId)
setChecked("channels/" + channelId)
for (const pubkey of pubkeys) {
loadInboxRelaySelections(pubkey)
Expand All @@ -46,7 +46,7 @@
})
onDestroy(() => {
markChannelRead(channelId)
setChecked("channels/" + channelId)
})
document.title = `Direct Messages`
Expand Down
5 changes: 4 additions & 1 deletion src/app/views/ChannelsList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {toTitle} from "hurdak"
import {derived} from "svelte/store"
import {signer} from "@welshman/app"
import {now} from "@welshman/lib"
import {createScroller} from "src/util/misc"
import Tabs from "src/partials/Tabs.svelte"
import Anchor from "src/partials/Anchor.svelte"
Expand All @@ -11,7 +12,7 @@
import Content from "src/partials/Content.svelte"
import ChannelsListItem from "src/app/views/ChannelsListItem.svelte"
import {router} from "src/app/util/router"
import {channels, hasNewMessages, markAllChannelsRead} from "src/engine"
import {channels, hasNewMessages, setChecked} from "src/engine"
const activeTab = window.location.pathname.slice(1) === "channels" ? "conversations" : "requests"
const setActiveTab = tab => {
Expand Down Expand Up @@ -39,6 +40,8 @@
}
})
const markAllChannelsRead = () => setChecked("channels/*", now())
document.title = "Direct Messages"
</script>

Expand Down
31 changes: 6 additions & 25 deletions src/app/views/NotificationSectionMain.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import {derived} from "svelte/store"
import {max, ago, int, HOUR, pushToMapKey} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {getAncestorTagValues, SEEN_GENERAL} from "@welshman/util"
import {getAncestorTagValues} from "@welshman/util"
import NotificationList from "src/app/views/NotificationList.svelte"
import NotificationMention from "src/app/views/NotificationMention.svelte"
import NotificationReplies from "src/app/views/NotificationReplies.svelte"
import {mainNotifications, unreadMainNotifications, markAsSeen} from "src/engine"
import {mainNotifications, setChecked} from "src/engine"
export let limit
Expand All @@ -34,31 +34,12 @@
})
})
let loading = false
onMount(() => {
const tracked = new Set()
const unsub = unreadMainNotifications.subscribe(async events => {
const untracked = events.filter(e => !tracked.has(e.id))
if (!loading && untracked.length > 0) {
for (const id of untracked) {
tracked.add(id)
}
loading = true
setChecked("notes/*")
await markAsSeen(SEEN_GENERAL, {
mentions: $mainNotifications,
replies: $mainNotifications,
})
loading = false
}
})
return unsub
return () => {
setChecked("notes/*")
}
})
</script>

Expand Down
32 changes: 7 additions & 25 deletions src/app/views/NotificationSectionReactions.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script lang="ts">
import {onMount} from "svelte"
import {derived} from "svelte/store"
import {partition, max, pushToMapKey} from "@welshman/lib"
import {max, pushToMapKey} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {getAncestorTagValues, SEEN_GENERAL, REACTION} from "@welshman/util"
import {getAncestorTagValues} from "@welshman/util"
import {formatTimestampAsDate} from "src/util/misc"
import NotificationList from "src/app/views/NotificationList.svelte"
import NotificationReactions from "src/app/views/NotificationReactions.svelte"
import {reactionNotifications, unreadReactionNotifications, markAsSeen} from "src/engine"
import {reactionNotifications, setChecked} from "src/engine"
export let limit
Expand All @@ -32,30 +32,12 @@
})
})
let loading = false
onMount(() => {
const tracked = new Set()
const unsub = unreadReactionNotifications.subscribe(async events => {
const untracked = events.filter(e => !tracked.has(e.id))
if (!loading && untracked.length > 0) {
for (const id of untracked) {
tracked.add(id)
}
const [reactions, zaps] = partition(e => e.kind === REACTION, $reactionNotifications)
loading = true
setChecked("reactions/*")
await markAsSeen(SEEN_GENERAL, {reactions, zaps})
loading = false
}
})
return unsub
return () => {
setChecked("reactions/*")
}
})
</script>

Expand Down
2 changes: 2 additions & 0 deletions src/app/views/Onboarding.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
broadcastUserData,
} from "src/engine"
import {router} from "src/app/util/router"
import {setChecked} from "src/engine"
export let stage = "intro"
export let invite = null
Expand Down Expand Up @@ -68,6 +69,7 @@
// Start our notifications listener
listenForNotifications()
setChecked("*")
}
onMount(async () => {
Expand Down
100 changes: 3 additions & 97 deletions src/engine/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type {Session} from "@welshman/app"
import {
follow as baseFollow,
unfollow as baseUnfollow,
ensurePlaintext,
getRelayUrls,
inboxRelaySelectionsByPubkey,
nip46Perms,
Expand All @@ -15,34 +14,16 @@ import {
userInboxRelaySelections,
userRelaySelections,
} from "@welshman/app"
import {
identity,
append,
cached,
ctx,
equals,
first,
groupBy,
indexBy,
last,
now,
nthEq,
nthNe,
remove,
splitAt,
} from "@welshman/lib"
import {identity, append, cached, ctx, groupBy, now, nthEq, remove} from "@welshman/lib"
import {Nip01Signer, Nip46Broker, Nip59, makeSecret} from "@welshman/signer"
import type {Profile, TrustedEvent} from "@welshman/util"
import {
Address,
DIRECT_MESSAGE,
FEEDS,
FOLLOWS,
INBOX_RELAYS,
LOCAL_RELAY_URL,
PROFILE,
RELAYS,
SEEN_CONVERSATION,
addToListPublicly,
createEvent,
createProfile,
Expand All @@ -57,26 +38,20 @@ import {
uniqTags,
} from "@welshman/util"
import crypto from "crypto"
import {Fetch, seconds, sleep, tryFunc} from "hurdak"
import {Fetch, sleep, tryFunc} from "hurdak"
import {assoc, flatten, omit, prop, reject, uniq} from "ramda"
import {
addClientTags,
anonymous,
channels,
createAndPublish,
getChannelIdFromEvent,
getChannelSeenKey,
getClientTags,
hasNip44,
publish,
sign,
userFeedFavorites,
userSeenStatusEvents,
withIndexers,
} from "src/engine/state"
import {sortEventsDesc} from "src/engine/utils"
import {blobToFile, stripExifData} from "src/util/html"
import {joinPath, parseJson} from "src/util/misc"
import {joinPath} from "src/util/misc"
import {appDataKeys} from "src/util/nostr"
import {get} from "svelte/store"

Expand Down Expand Up @@ -367,47 +342,6 @@ export const joinRelay = async (url: string, claim?: string) => {
}
}

// Read receipts

export const markAsSeen = async (kind: number, eventsByKey: Record<string, TrustedEvent[]>) => {
if (!get(hasNip44) || Object.entries(eventsByKey).length === 0) {
return
}

const cutoff = now() - seconds(180, "day")
const prev = get(userSeenStatusEvents).find(e => e.kind === kind)
const prevTags = prev ? parseJson(await ensurePlaintext(prev))?.filter?.(nthNe(1, "*")) : []
const data = indexBy(t => t[1], prevTags || [])

for (const [key, events] of Object.entries(eventsByKey)) {
if (events.length === 0) {
continue
}

const [newer, older] = splitAt(1, sortEventsDesc(events))
const ts = first(older)?.created_at || last(newer).created_at - seconds(3, "hour")

if (ts >= cutoff) {
data.set(key, ["seen", key, String(ts), ...newer.map(e => e.id)])
} else {
data.delete(key)
}
}

const tags = Array.from(data.values())

if (equals(tags, prevTags)) {
return
}

// Wait until after comparing for equality to add our current timestamp
const json = JSON.stringify([...tags, ["seen", "*", String(cutoff)]])
// const relays = ctx.app.router.FromUser().getUrls()
const content = await signer.get().nip44.encrypt(pubkey.get(), json)

await createAndPublish({kind, content, relays: [LOCAL_RELAY_URL]})
}

// Messages

export const sendMessage = async (channelId: string, content: string, delay: number) => {
Expand Down Expand Up @@ -436,34 +370,6 @@ export const sendMessage = async (channelId: string, content: string, delay: num
}
}

export const markChannelsRead = (ids: Set<string>) => {
const $pubkey = pubkey.get()
const eventsByKey = {}

for (const {id, last_sent = 0, last_received = 0, last_checked = 0} of get(channels)) {
if (!ids.has(id) || Math.max(last_sent, last_checked) > last_received) {
continue
}

const members = id.split(",")
const key = getChannelSeenKey(id)
const since = Math.max(last_sent, last_checked)
const events = repository
.query([{kinds: [4, DIRECT_MESSAGE], authors: members, "#p": members, since}])
.filter(e => getChannelIdFromEvent(e) === id && e.pubkey !== $pubkey)

if (events.length > 0) {
eventsByKey[key] = events
}
}

markAsSeen(SEEN_CONVERSATION, eventsByKey)
}

export const markAllChannelsRead = () => markChannelsRead(new Set(get(channels).map(c => c.id)))

export const markChannelRead = (id: string) => markChannelsRead(new Set([id]))

// Session/login

const addSession = (s: Session) => {
Expand Down
1 change: 1 addition & 0 deletions src/engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from "src/engine/utils"
export * from "src/engine/state"
export * from "src/engine/requests"
export * from "src/engine/commands"
export * from "src/engine/notifications"
Loading

0 comments on commit 8ff7d5e

Please sign in to comment.