diff options
| author | Adam Soltys <[email protected]> | 2026-08-03 22:07:13 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-03 22:07:13 -0700 |
| commit | 1f96f513060d65c3a4e7c7cda100cde4ef8ee2c3 (patch) | |
| tree | 04bb4bae281d9255ca45f9100c6903191b5c7b74 /src/features/messages.js | |
| parent | dc8f4b841ff57f4ef74597c2f80b6f90b352db13 (diff) | |
| download | coinosv3-1f96f513060d65c3a4e7c7cda100cde4ef8ee2c3.tar.gz coinosv3-1f96f513060d65c3a4e7c7cda100cde4ef8ee2c3.zip | |
Only people you know get to buzz your phone
A NIP-17 wrap is signed by a throwaway key with the sender sealed under
two layers of nip44, so no server can filter DMs by who sent them. The
notifier now ships the wrap inside the push payload and the service
worker opens it locally: a message from someone you follow arrives with
their name on it, a stranger's is dropped, and your own sent-copy — every
DM is wrapped to the sender too — no longer notifies you about what you
just typed.
That needs the nostr identity key in the worker's IndexedDB, which
dm-inbox.js states plainly: for a seed-derived identity it is a second
copy of a key localStorage's seed already yields, and it exists only to
keep strangers out. A remote signer never hands its key over, so those
wallets store no secret and keep getting unfiltered notifications rather
than silence.
Silence is always the riskier failure here, so the rule leans the other
way whenever it isn't sure: a wrap we can't open notifies, an account
with no contact list published notifies (otherwise a new user's welcome
message would vanish), and anyone you already have a thread with counts
as known whether or not you follow them. The server-side DM cooldown
drops to a second — with the device discarding pushes, a throttle there
would let a discarded one swallow the message behind it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'src/features/messages.js')
| -rw-r--r-- | src/features/messages.js | 36 |
1 files changed, 36 insertions, 0 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 70886d3..5aa8dfe 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -23,6 +23,7 @@ import { communityId, parseInviteLink, makeInviteLink, makeInviteBundleEvent, openInviteBundle, } from '../concord.js'; import { makeDM, unwrapDM, wrapDM } from '../dm.js'; +import { saveInbox } from '../dm-inbox.js'; import { makeSearcher, resultRows, fallbackAvatar } from '../recipient-search.js'; import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; import { t } from '../i18n.js'; @@ -607,6 +608,37 @@ export function messagesFeature(ctx) { } } + // Hand the service worker what it needs to tell a friend's DM from a + // stranger's: our key (only if we actually hold one), who we follow, and the + // names we already know. See dm-inbox.js for the threat model — a remote + // signer stores no key here and simply gets unfiltered notifications. + let inboxAt = 0; + async function syncInbox({ force = false } = {}) { + if (!force && Date.now() - inboxAt < 5 * 60_000) return; + inboxAt = Date.now(); + try { + const id = await identity(); + if (!id) return; + const evs = await queryOn([...new Set([...PROFILE_RELAYS, ...DM_RELAYS])], + { kinds: [3], authors: [id.pubkey] }, 4000); + const newest = evs.sort((a, b) => b.created_at - a.created_at)[0]; + const follows = newest + ? [...new Set(newest.tags.filter((x) => x[0] === 'p' && /^[0-9a-f]{64}$/.test(x[1] || '')).map((x) => x[1]))] + : []; + const known = [...threads.keys()]; + const names = {}; + for (const pk of [...follows, ...known]) { + const p = profiles.get(pk); + if (p && p.name) names[pk] = p.name; + } + await saveInbox(wallet._cacheKey(), { + pubkey: id.pubkey, + sk: id.signer instanceof Uint8Array ? bytesToHex(id.signer) : null, + follows, known, names, hasList: !!newest, updated: Date.now(), + }); + } catch { inboxAt = 0; } + } + async function registerPush({ interactive = false } = {}) { try { if (typeof Notification === 'undefined' || !('serviceWorker' in navigator) || !('PushManager' in window)) return false; @@ -1205,6 +1237,9 @@ export function messagesFeature(ctx) { function homeView() { startDMs(); + // Threads you've since replied to should stop being strangers to the + // worker; throttled inside, so this is cheap on every render. + syncInbox().catch(() => {}); for (const jm of communities()) ensureRoom(jm); const dmRows = [...threads.entries()] @@ -1639,6 +1674,7 @@ export function messagesFeature(ctx) { // header's unread dot can't report a room nobody is listening to. for (const jm of communities()) { try { ensureRoom(jm); } catch {} } syncLists().catch(() => {}); + syncInbox().catch(() => {}); registerPush().catch(() => {}); // silent refresh when permission already granted }, stop() { |
