The last Amber pain: a fresh launch re-decrypted the whole inbox (2 signer ops
per wrap). Decrypted messages are now persisted to IndexedDB
(src/lib/dmDecryptCache.ts, keyed viewerPk:wrapId, bounded 4000/account, pruned
oldest-first) and hydrated once per session before the first lookup, so a wrap is
decrypted at most once ever. Cleared per-account on logout (plaintext at rest
otherwise readable by another account on the device).
Per the crew-chat audit recommendations:
- The crew Chat tab no longer silently switches transport on a coordinator tag.
It is always NIP-17; coordinator routing lives only in the Private chat (MLS).
useCordnChat / useCrewChatUnread / lib/cordn client+crypto are kept but unwired
(headers marked). Drops the tab's Cordn-only unread dot.
- Group reads (Groups list + crew thread) widened 200 -> 500 wraps; the shared
decrypt cache makes the extra reads affordable. 1:1 list stays 200.
Audit of the crew Chat tab (see CREW_CHAT_AUDIT.md):
- A sent crew message never appeared: useSendDirectMessage invalidated the list
keys but not ['group-messages', gid, ...] (the thread key). Now optimistically
inserts the sender's own plaintext (no refetch -> no Amber pop-ups).
- Every reader decrypted every wrap under its own query key, so each view
(Messages, then each crew thread) re-decrypted the whole inbox. Added a shared,
coalesced, bounded session decrypt cache (decryptGiftWrap), successes only.
- Threads dedup by rumor id; CrewChat guards double-send with a ref; the Cordn
path optimistically inserts and invalidates only for silent signers.
Sending to a crew fired every recipient's gift wrap in parallel (Promise.all),
and the sealed-relay crew chat encrypted to the whole roster in parallel. Amber
is a single foreground activity: concurrent nostrsigner: intents mean only one is
answered and the rest sit until the 60s native timeout rejects with 'Amber didn't
respond'. Both paths now go sequentially for an interactive signer (parallel kept
for silent signers). Also corrected the approval hint (2 per member: encrypt +
sign, not 1 per recipient).
For an interactive signer (Amber) each nip44 decrypt opens the app. The DM/NIP-17
read hooks decrypt 2x per gift wrap and DirectMessagesPage mounts two of them, so
navigating to Messages stormed Amber. Strengthened amberSafeQueryOptions for
interactive signers (staleTime/gcTime Infinity, refetchOnMount/refetchOnReconnect
false) so decryption happens once per session, mirroring useStash; same guard on
usePrivateContacts + useCordnChat. Added a manual Refresh to the Messages header
(Amber gets no polling). Gated useSyncPendingPosts' legacy re-sign loop for
interactive signers.
Verified the legacy relay held the same 115 events as the new one (0 diff) before removing it, so no data is lost. Drops wss://goskatespots.nostr1.com from the default pools, the DM/CVM/zap relay arrays, and the cvm-server + push-service defaults.
Point SKATE_RELAY, the default relay pools, the DM/CVM/zap relay arrays, the cvm-server + push-service defaults, and the archive default at wss://relay.skate-spots.org, keeping the legacy hosted relay as a fallback during the client-update window. Restored 115/115 archived events; the relay runs behind Caddy with TLS. Docs + AGENTS.md updated.
- C1 Marketplace payments now verify the invoice amount: usePayPaymentPointer
calls assertInvoiceAmount(invoice, amountSats) before paying, so a seller
can't attach a raw BOLT11 or return an inflated LNURL invoice and have an
auto-approving wallet pay it.
- C2 NIP-17 DMs now verifyEvent(seal) in all four read paths — the seal is the
only authentication for a message; a crafted seal could impersonate the sender.
- C3 push-service: bind endpoint ownership (reject subscribe by a different
pubkey; only the owner may unsubscribe) and reject non-HTTPS/private/loopback
endpoints (SSRF).
- C4 Amber callback: a bare ?event=<pubkey> is no longer treated as a login;
the callback must match a pending get_public_key op (both callback pages).
Also stops logging the pending op payload.
- C5a platform-fee record is now per-pubkey, so an account on a shared device
can't inherit another's paid status.
- C5b (self-asserted featured ranking) deferred: needs a batch-verification pass
against provider zap receipts before it can gate ordering.
- public/push-sw.js: shows the content-free push; tap focuses/opens the app.
- src/lib/pushService.ts + usePushNotifications: register the SW, subscribe with
the service's VAPID key, sign a NIP-98 challenge proof, POST /subscribe.
- NotificationSettingsCard: a 'Background notifications' toggle (opt-in,
per-device) + honest metadata copy; corrected the now-stale 'we run no push
server' note.
- useSendDirectMessage: marks the RECIPIENT wrap ["skatespots","push"] (never
the self copy) and adds the community relay as the notify relay so the push
service sees it.
- /privacy: discloses the push server sees metadata only.
Mirrors the activity the app already computes (bell, crews, DMs, newsletter)
to the OS notification shade, with no new relay traffic and no new network
destination.
- src/lib/osNotify.ts: one delivery API. Web uses the Notification API; native
uses @capacitor/local-notifications (lazily imported, so the web bundle never
pays for it). Tap-through navigates in-app.
- src/components/OsNotificationBridge.tsx: mounted once in Layout, inert until
opted in. Per-source silent priming + a seen-id set means history is never
replayed as a flood, and it stays quiet while the app is visible (the bell
already covers that).
- DMs notify from the gift-wrap ENVELOPE only (src/hooks/useDmEnvelopes.ts,
no decryption) — so Amber is never prompted just to be told you have a
message. Preview is honestly generic.
- src/lib/notificationPrefs.ts + useNotificationPrefs: device-local, opt-in,
default off, per-category toggles. Settings -> Notifications UI.
Deliberately local-only: a fully closed app needs a server push, which is not
built (Phase 2 = on-device background runner; Phase 3 = content-free FCM).
All four DM read hooks used config.relayMetadata.dmRelays, which is only set by
the login-time sync. When it is stale/empty the app reads only NIP-65 relays —
where gift wraps almost never are (real case: 94 addressed wraps on nostr.mom,
0-1 on the user's NIP-65 relays), so Messages showed 'No messages yet' with a
full inbox.
- useResolvedDmRelays(): discovers the user's own kind-10050 inbox relays at
query time (per-relay, 6s each, over read relays + aggregators) and unions
them in. Used by useConversation/useConversations/useGroupConversations/
useGroupConversation.
- useConversations now returns { conversations, undecryptable, wraps } and the
DM page surfaces 'N encrypted messages couldn't be opened', so a decrypt
failure stops looking identical to an empty inbox. Updated DirectMessagesPage
and ContactsPage.
Root cause: AppProvider validates the persisted config with
AppConfigSchema.partial().parse(), and RelayMetadataSchema only declared
'relays' + 'updatedAt'. Zod drops unknown keys by default, so dmRelays (and
localRelays + blockedRelays) were SILENTLY STRIPPED on every page load. The DM
inbox relays (kind 10050) are usually not in a user's NIP-65 list, so DM reads
and writes fell back to NIP-65 relays - where the other side isn't listening -
and messages looked like they never went through.
- Declare localRelays/blockedRelays/dmRelays in RelayMetadataSchema.
- Send: publish gift wraps in PARALLEL with per-relay timeouts and resolve on
the first relay that accepts (a dead relay or auth.nostr1.com no longer
stalls the send); deliver to the recipient's NIP-65 WRITE relays when they
have no discoverable 10050 (the old fallback was dead code because our own DM
relays are always non-empty); discovery of 10050/10002 now runs in parallel.
- Read: query all DM relays in parallel with per-relay timeouts. Previously one
slow/dead relay burned the shared 15s budget and then the fired abort made
every remaining relay fail instantly.
Verified with an nsec login + a hostile relay list (dead + auth-required):
before, the wrap reached only write relays and the recipient found 0; after, it
lands on the configured inbox relays and both the recipient and our own copy are
found. Send worst case 20s -> 4.5s.
- New '/trades' page: action-needed (incoming 'confirm this sale' requests as
seller, pending confirmations as buyer) with inline actions, a reputation
card, and the user's own trade records (role/counterparty/state) linking to
each listing. Nav entry (auth-only) + smoke route.
- DM inbox relays (kind 10050) are now block-filtered on both read and delivery
(closes the last relay caveat).
- AGENTS.md updated.
Gate: 101 tests, 0 errors/warnings, boot smoke green.
So Local relays are used and Blocked relays are excluded EVERYWHERE, not just in
the NPool routers:
- useListingReports, useSellerInfo, useSkateRelays (read+write), useZaps
(NIP-57 relays tag), usePurchasePro, CollectionDetailPage, CrewForm,
useDirectMessages write fallback
- AppProvider's empty-relays defaults check is the only intentional direct reader
- AGENTS.md updated
Gate: 101 tests, 0 errors/warnings, boot smoke green.
- RelayMetadata.dmRelays; Relays UI to add/remove, publishing kind 10050
- NostrSync fetches the user's 10050 on login (alongside 10002)
- DMs deliver gift wraps to recipients' + own DM relays (fallback: write relays)
and read from own DM relays + NIP-65 read relays
- useDmRelays() to look up others' DM relay lists
Also fixes a latent data-loss bug: NostrSync's 10002 fetch replaced
relayMetadata wholesale, which would wipe the new local/blocked/dm lists on
login — it now merges. 'Trusted' category intentionally skipped (no distinct
behaviour -> no empty UI); documented in AGENTS.md.
Uses the existing gift-wrap primitive (kind 14 rumor, one p-tag per recipient):
- useGroupConversations: lists multi-recipient threads, bucketed by the sorted
participant key; the inbox shows a Groups section
- GroupConversationPage (/messages/group/:participants): encrypted group thread
with sender names, send + error feedback
- smoke test now boots the new route
Group creation UI (multi-select in NewMessageDialog) is the next step. For
stronger guarantees (forward secrecy, rosters) Marmot/MLS or Cordn would be the
protocol to adopt later; noted in the hook.
NIP-17 group messages (rumor with >1 recipient p-tag) were bleeding into the
1:1 surfaces:
- useConversation (thread view): a group message from `otherPubkey` where I was
also a recipient satisfied isFromOther && isToMe, so group chatter appeared
inside the 1:1 thread.
- useConversations (list): group messages were collapsed to conversationWith =
sender (or an arbitrary recipient for my own sends), spawning phantom 1:1
entries and double-listing every group (once here, once in
useGroupConversations).
Both 1:1 hooks now skip any rumor with more than one p-tag. Groups remain the
sole responsibility of the useGroupConversation(s) hooks, which already require
pTags.length > 1. Genuine 1:1 rumors carry exactly one recipient p-tag
(createRumor doesn't tag the sender), so no legitimate message is dropped.
Verified solid (no changes): the seal(13)/rumor(14) kind checks, the
rumor.pubkey === seal.pubkey impersonation guard, and the amberSafeQueryOptions
decrypt-storm protection for interactive signers are all correct.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
With Amber as signer, every nip44 decrypt is a foreground intent (visible
popup). Several hooks decrypt inside auto-running queries, so Amber flashed on
navigation/focus and (worst case) storms — one popup per encrypted event, and
polling queries re-fired every few seconds.
New helper isInteractiveSigner() (signerCapabilities.ts) detects the Amber
(x-amber) signer, where each crypto op prompts the user. Hooks now gate passive
decryption for interactive signers only; extension/nsec/bunker keep their
silent, live-polling behaviour.
Fixes:
- useCrewDismissals (GLOBAL via header NotificationBell — the main offender):
Amber users now use the instant localStorage mirror only, never passively
decrypt the synced list; disabled refetchOnWindowFocus + Infinity staleTime.
Dismissals still sync OUT on the write path.
- useDirectMessages (all 4 DM query hooks): added amberSafeQueryOptions() —
no background polling and no focus refetch for Amber (these decrypt up to
~200 gift wraps × 2 each); silent signers keep the 60s poll.
- useStash / usePrivateContacts: disabled focus refetch + Infinity staleTime
for Amber so the single decrypt runs once, not on every focus.
- useCordnChat: dropped the 5s live poll for Amber (was a popup every 5s);
silent signers keep it.
Scanned every signEvent call site: all are inside mutations/user callbacks
(zaps, invites, DMs, chat, follows, purchases, NIP-98 auth) — no passive
auto-signing anywhere. Deliberate user actions still prompt Amber as intended.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
#2 (crash): useDirectMessages called getEventHash() without importing it —
`getEventHash is not defined` broke crew chat and any NIP-17 DM send. Added
the missing import from nostr-tools.
#3 (bell not loading on phone): the notifications popover had a fixed-height
scroll area and no collision handling, so on small screens it rendered clipped
against the top edge with the tab content cut off. The popover now constrains
to the Radix available-height, uses a flex column, and the tab list/scroll area
flex correctly — all tabs (incl. Crews/News) are reachable on mobile.
#4 (multi-account on phone): the mobile header user menu had only Set Status +
Log Out. Added an Accounts section that lists other logged-in npubs to switch
between, plus "Add another account" which opens the login dialog. Desktop
already had this via AccountSwitcher; mobile now reaches parity.
Note: #1/#5/#6 (stuck on publish/vouch/delete) were reported against the shipped
1.2.2 APK, which also contained the #2 crash. Rebuild 1.2.3 and re-test — the
sign/publish paths in current source are correct (Amber round-trip has a 90s
timeout + surfaced errors).
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
Root cause: NUser's extension signer exposes .nip44 as a getter that calls
.nostr (window.nostr) and THROWS when the extension isn't present. Any
synchronous access during render crashes the whole tree — the actual culprit
was useStash's `isEncryptionAvailable: !!user?.signer?.nip44` in its return
object, evaluated every render even when logged out.
- Add src/lib/signerCapabilities.ts: hasNip44/hasNip04 helpers that determine
capability via login type + direct window.nostr probe (no throwing getter).
- useStash: use the helper for hasNip44 + isEncryptionAvailable
- useDirectMessages: replace all render-path `enabled: !!user?.signer?.nip44`
with a pre-computed nip44Available across all 4 conversation hooks
- CrewChat, CrewRequestToJoin, ConversationPage, DirectMessagesPage,
ContactsPage: use the safe helper in render/guard paths
Async accesses (inside queryFn/callbacks) are left as-is — they reject
gracefully within react-query and never crash render.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
@noble/curves v2 dropped the ./secp256k1 subpath export. The manual
getEventHash + schnorr.sign + bytesToHex pattern is replaced with
nostr-tools' finalizeEvent() which handles id computation and signing
internally, with no external subpath dependencies. Also removes the
now-unused getEventHash import and bytesToHex helper.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
@noble/hashes v2 dropped the ./utils subpath export, breaking the import
in useDirectMessages.ts. Replaced with a simple inline implementation
that has zero new dependencies and works correctly with any build tool.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.
Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)
Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +07:00
Renamed from src/src/hooks/useDirectMessages.ts (Browse further)