From 7916ed501ce0073690a23e299919064527a74cb6 Mon Sep 17 00:00:00 2001 From: Jonathan Staab Date: Thu, 20 Apr 2023 14:50:02 -0500 Subject: [PATCH] Remember message/chat notification read status --- CHANGELOG.md | 1 + ROADMAP.md | 3 +- src/agent/cmd.ts | 5 +- src/agent/network.ts | 20 +++++--- src/agent/sync.ts | 49 +++++++++++-------- src/agent/user.ts | 25 ++++++++-- src/app/Routes.svelte | 7 ++- src/app/state.ts | 69 +++++++++++---------------- src/app/views/ChatDetail.svelte | 7 +-- src/app/views/ChatListItem.svelte | 16 +------ src/app/views/MessagesDetail.svelte | 19 ++++---- src/app/views/MessagesList.svelte | 20 +++----- src/app/views/MessagesListItem.svelte | 6 ++- src/app/views/Notifications.svelte | 9 ++-- src/partials/Channel.svelte | 4 +- src/util/nostr.ts | 1 + 16 files changed, 140 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c443fcdb..e3fe1744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - [x] Add purplepag.es to sign in flow - [x] Include people with only a display_name in search - [x] Fix AUTH over multiplextr +- [x] Remember whether messages/notifications have been read ## 0.2.24 diff --git a/ROADMAP.md b/ROADMAP.md index e75c9ebf..d84a9b4f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,7 +1,7 @@ # Current -- [ ] Remember message/chat status - [ ] Add way to turn off likes/zaps +- [ ] Remember joined rooms - [ ] Image classification - https://github.com/bhky/opennsfw2 - [ ] Claim relays bounty @@ -44,6 +44,7 @@ # UI/Features +- [ ] Use real links so cmd+click or right click work - [ ] Allow sharing of lists/following other people's lists - [ ] Add suggestion list for topics on compose - [ ] Badges link to https://badges.page/p/97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322 diff --git a/src/agent/cmd.ts b/src/agent/cmd.ts index 7a1c8542..402f386f 100644 --- a/src/agent/cmd.ts +++ b/src/agent/cmd.ts @@ -31,8 +31,7 @@ const setRelays = newRelays => }), }) -const setSettings = content => - new PublishableEvent(30078, {content, tags: [["d", "coracle/settings/v1"]]}) +const setAppData = (d, content) => new PublishableEvent(30078, {content, tags: [["d", d]]}) const setPetnames = petnames => new PublishableEvent(3, {tags: petnames}) @@ -203,7 +202,7 @@ export default { authenticate, updateUser, setRelays, - setSettings, + setAppData, setPetnames, setMutes, createList, diff --git a/src/agent/network.ts b/src/agent/network.ts index 6600b087..970d9ebc 100644 --- a/src/agent/network.ts +++ b/src/agent/network.ts @@ -1,6 +1,6 @@ import type {MyEvent} from "src/util/types" -import {sortBy, assoc, uniq, uniqBy, prop, propEq, groupBy, pluck} from "ramda" -import {personKinds, findReplyId} from "src/util/nostr" +import {without, sortBy, assoc, uniq, uniqBy, prop, propEq, groupBy, pluck} from "ramda" +import {personKinds, appDataKeys, findReplyId} from "src/util/nostr" import {chunk} from "hurdak/lib/hurdak" import {batch, now, timedelta} from "src/util/misc" import { @@ -107,10 +107,18 @@ const loadPeople = async (pubkeys, {relays = null, kinds = personKinds, force = await Promise.all( chunk(256, pubkeys).map(async chunk => { - await load({ - relays: sampleRelays(relays || getAllPubkeyWriteRelays(chunk), 0.5), - filter: {kinds, authors: chunk}, - }) + const chunkRelays = sampleRelays(relays || getAllPubkeyWriteRelays(chunk), 0.5) + const chunkFilter = [] as Array> + + chunkFilter.push({kinds: without([30078], kinds), authors: chunk}) + + // Add a separate filter for app data so we're not pulling down other people's stuff, + // or obsolete events of our own. + if (kinds.includes(30078)) { + chunkFilter.push({kinds: [30078], authors: chunk, "#d": appDataKeys}) + } + + await load({relays: chunkRelays, filter: chunkFilter}) }) ) } diff --git a/src/agent/sync.ts b/src/agent/sync.ts index e0adcc9a..a4dbc37b 100644 --- a/src/agent/sync.ts +++ b/src/agent/sync.ts @@ -149,27 +149,29 @@ addHandler(3, e => { // User profile, except for events also handled for other users -const profileHandler = (key, getValue) => async e => { - const profile = user.getProfile() - - if (e.pubkey !== profile.pubkey) { - return - } - - const updated_at_key = `${key}_updated_at` - - if (e.created_at < profile?.[updated_at_key]) { - return - } - - const value = await getValue(e, profile) - - // If we didn't get a value, don't update the key - if (value) { - user.profile.set({...profile, [key]: value, [updated_at_key]: e.created_at}) +const userHandler = cb => e => { + if (e.pubkey === user.getPubkey()) { + cb(e) } } +const profileHandler = (key, getValue) => + userHandler(async e => { + const profile = user.getProfile() + const updated_at_key = `${key}_updated_at` + + if (e.created_at < profile?.[updated_at_key]) { + return + } + + const value = await getValue(e, profile) + + // If we didn't get a value, don't update the key + if (value) { + user.profile.set({...profile, [key]: value, [updated_at_key]: e.created_at}) + } + }) + addHandler( 2, profileHandler("relays", (e, p) => uniqByUrl(p.relays.concat({url: e.content}))) @@ -245,6 +247,15 @@ addHandler( }) ) +addHandler( + 30078, + profileHandler("lastChecked", async (e, p) => { + if (Tags.from(e).getMeta("d") === "coracle/last_checked/v1") { + return {...p.lastChecked, ...(await keys.decryptJson(e.content))} + } + }) +) + // Rooms addHandler(40, e => { @@ -269,7 +280,7 @@ addHandler(40, e => { }) addHandler(41, e => { - const roomId = Tags.from(e).type("e").values().first() + const roomId = Tags.from(e).getMeta("e") if (!roomId) { return diff --git a/src/agent/user.ts b/src/agent/user.ts index 2226cc28..a8930155 100644 --- a/src/agent/user.ts +++ b/src/agent/user.ts @@ -34,6 +34,7 @@ const profile = synced("agent/user/profile", { dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL, multiplextrUrl: import.meta.env.VITE_MULTIPLEXTR_URL, }, + lastChecked: {}, petnames: [], relays: [], mutes: [], @@ -41,6 +42,7 @@ const profile = synced("agent/user/profile", { }) const settings = derived(profile, prop("settings")) +const lastChecked = derived(profile, prop("lastChecked")) as Readable> const petnames = derived(profile, prop("petnames")) as Readable>> const relays = derived(profile, prop("relays")) as Readable> const mutes = derived(profile, prop("mutes")) as Readable> @@ -85,12 +87,29 @@ export default { async setSettings(settings) { profile.update($p => ({...$p, settings})) - if (keys.canSign()) { - const content = await keys.encryptJson(settings) + return this.setAppData("settings/v1", settings) + }, - return cmd.setSettings(content).publish(profileCopy.relays) + // App data + + lastChecked, + async setAppData(key, content) { + if (keys.canSign()) { + const d = `coracle/${key}` + const v = await keys.encryptJson(content) + + return cmd.setAppData(d, v).publish(profileCopy.relays) } }, + setLastChecked(k, v) { + profile.update($profile => { + const lastChecked = {...$profile.lastChecked, [k]: v} + + this.setAppData("last_checked/v1", lastChecked) + + return {...$profile, lastChecked} + }) + }, // Petnames diff --git a/src/app/Routes.svelte b/src/app/Routes.svelte index 203668e8..759b637d 100644 --- a/src/app/Routes.svelte +++ b/src/app/Routes.svelte @@ -57,7 +57,12 @@ {/key} - + + + + + + {#key params.entity} diff --git a/src/app/state.ts b/src/app/state.ts index 1d529e09..1a293f92 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -4,11 +4,11 @@ import {nip19} from "nostr-tools" import {navigate} from "svelte-routing" import {derived} from "svelte/store" import {writable} from "svelte/store" -import {omit, pluck, sortBy, max, find, slice, propEq} from "ramda" +import {max, omit, pluck, sortBy, find, slice, propEq} from "ramda" import {createMap, doPipe, first} from "hurdak/lib/hurdak" import {warn} from "src/util/logger" import {hash} from "src/util/misc" -import {synced, now, timedelta} from "src/util/misc" +import {now, timedelta} from "src/util/misc" import {Tags, isNotification, userKinds} from "src/util/nostr" import {findReplyId} from "src/util/nostr" import {modal, toast} from "src/partials/state" @@ -96,21 +96,24 @@ export const feedsTab = writable("Follows") // State -export const lastChecked = synced("app/alerts/lastChecked", {}) - export const newNotifications = derived( - [watch("notifications", t => pluck("created_at", t.all()).reduce(max, 0)), lastChecked], + [watch("notifications", t => pluck("created_at", t.all()).reduce(max, 0)), user.lastChecked], ([$lastNotification, $lastChecked]) => $lastNotification > ($lastChecked.notifications || 0) ) +export const hasNewMessages = ({lastReceived, lastSent}, lastChecked) => + lastReceived > Math.max(lastSent, lastChecked || 0) + export const newDirectMessages = derived( - [watch("contacts", t => t.all()), lastChecked], - ([contacts, $lastChecked]) => Boolean(find(c => c.lastMessage > $lastChecked[c.pubkey], contacts)) + [watch("contacts", t => t.all()), user.lastChecked], + ([contacts, $lastChecked]) => + Boolean(find(c => hasNewMessages(c, $lastChecked[`dm/${c.pubkey}`]), contacts)) ) export const newChatMessages = derived( - [watch("rooms", t => t.all()), lastChecked], - ([rooms, $lastChecked]) => Boolean(find(c => c.lastMessage > $lastChecked[c.pubkey], rooms)) + [watch("rooms", t => t.all()), user.lastChecked], + ([rooms, $lastChecked]) => + Boolean(find(r => hasNewMessages(r, $lastChecked[`chat/${r.id}`]), rooms)) ) // Synchronization from events to state @@ -126,23 +129,17 @@ const processMessages = async (pubkey, events) => { return } - lastChecked.update($lastChecked => { - for (const message of messages) { - if (message.pubkey === pubkey) { - const recipient = Tags.from(message).type("p").values().first() + for (const message of messages) { + const fromSelf = message.pubkey === pubkey + const contactPubkey = fromSelf ? Tags.from(message).getMeta("p") : message.pubkey + const contact = contacts.get(contactPubkey) + const key = fromSelf ? "lastSent" : "lastReceived" - $lastChecked[recipient] = Math.max($lastChecked[recipient] || 0, message.created_at) - contacts.patch({pubkey: recipient, accepted: true}) - } else { - const contact = contacts.get(message.pubkey) - const lastMessage = Math.max(contact?.lastMessage || 0, message.created_at) - - contacts.patch({pubkey: message.pubkey, lastMessage}) - } - } - - return $lastChecked - }) + contacts.patch({ + pubkey: contactPubkey, + [key]: Math.max(contact?.[key] || 0, message.created_at), + }) + } } const processChats = async (pubkey, events) => { @@ -152,22 +149,14 @@ const processChats = async (pubkey, events) => { return } - lastChecked.update($lastChecked => { - for (const message of messages) { - const id = Tags.from(message).getMeta("e") + for (const message of messages) { + const fromSelf = message.pubkey === pubkey + const id = Tags.from(message).getMeta("e") + const room = rooms.get(id) + const key = fromSelf ? "lastSent" : "lastReceived" - if (message.pubkey === pubkey) { - $lastChecked[id] = Math.max($lastChecked[id] || 0, message.created_at) - } else { - const room = rooms.get(id) - const lastMessage = Math.max(room?.lastMessage || 0, message.created_at) - - rooms.patch({id, lastMessage}) - } - } - - return $lastChecked - }) + rooms.patch({id, [key]: Math.max(room?.[key] || 0, message.created_at)}) + } } export const listen = async pubkey => { diff --git a/src/app/views/ChatDetail.svelte b/src/app/views/ChatDetail.svelte index be9ae3a9..0bd8ba4c 100644 --- a/src/app/views/ChatDetail.svelte +++ b/src/app/views/ChatDetail.svelte @@ -1,6 +1,4 @@ diff --git a/src/app/views/ChatListItem.svelte b/src/app/views/ChatListItem.svelte index f324d84e..66fb70c9 100644 --- a/src/app/views/ChatListItem.svelte +++ b/src/app/views/ChatListItem.svelte @@ -27,24 +27,12 @@

{room.name || ""}

{#if room.joined} - { - e.stopPropagation() - leave() - }}> + Leave {:else} - { - e.stopPropagation() - join() - }}> + Join diff --git a/src/app/views/MessagesDetail.svelte b/src/app/views/MessagesDetail.svelte index ee670b68..bd992e48 100644 --- a/src/app/views/MessagesDetail.svelte +++ b/src/app/views/MessagesDetail.svelte @@ -1,6 +1,5 @@ - + {#each getContacts(activeTab) as contact (contact.pubkey)} {:else} diff --git a/src/app/views/MessagesListItem.svelte b/src/app/views/MessagesListItem.svelte index 56e77f0d..e7abd368 100644 --- a/src/app/views/MessagesListItem.svelte +++ b/src/app/views/MessagesListItem.svelte @@ -4,15 +4,17 @@ import {ellipsize} from "hurdak/lib/hurdak" import {displayPerson} from "src/util/nostr" import {getPersonWithFallback} from "src/agent/db" - import {lastChecked} from "src/app/state" + import user from "src/agent/user" import PersonCircle from "src/app/shared/PersonCircle.svelte" import Card from "src/partials/Card.svelte" + import {hasNewMessages} from "src/app/state" export let contact - const newMessages = contact.lastMessage > $lastChecked[contact.pubkey] + const {lastChecked} = user const person = getPersonWithFallback(contact.pubkey) const enter = () => navigate(`/messages/${nip19.npubEncode(contact.pubkey)}`) + const newMessages = hasNewMessages(contact, $lastChecked[`dm/${contact.pubkey}`]) diff --git a/src/app/views/Notifications.svelte b/src/app/views/Notifications.svelte index c9e3b1b4..3541c753 100644 --- a/src/app/views/Notifications.svelte +++ b/src/app/views/Notifications.svelte @@ -1,5 +1,6 @@