Skip to content

Activity badges: count only what needs you; clear per account - #113

Merged
BitcoinErrorLog merged 7 commits into
release/shop-v0.6.8from
cursor/activity-badge-actionable-58ad
Sep 24, 2026
Merged

BitcoinErrorLog merged 7 commits into
release/shop-v0.6.8from
cursor/activity-badge-actionable-58ad

Conversation

@BitcoinErrorLog

Copy link
Copy Markdown
Owner

Summary

Fixes both badge problems Pav reported on v0.6.25 in pubky-marketplace#54:

  1. The badge counted history, not things that need you. It now counts only things that need the signed-in account right now. Old rows like "Checkout started" and "Order cancelled" never count.
  2. Clearing only applied to one browser. Opening Activity or Orders now saves a checkpoint for the account, and every browser picks it up.

No marketplace-service change was needed.

What counts as needing you

The set comes from the service's order state model (next_actor, the same value the order cards show as "Waiting on you") and from the offer state model.

Orders tab badge. One per order, when both of these hold:

  • the service's next_actor for the order is this account (as buyer or seller), and
  • the order changed after the account last opened Orders.

By state, that is:

State Who acts next
pending_payment Usually the buyer, who pays before the hold lapses. It is the seller when the buyer reported a payment, when a Paykit payment waits on seller confirmation, or when there is no payment method and the seller has no rail. A checkout whose holdExpiresAt has already passed does not count.
paid, processing, cancel_requested, return_requested, return_approved, return_received The seller. A payment in manual review also goes to the seller.
shipped, ready_for_pickup The buyer.
delivered, completed, cancelled, refunded_external, closed No one, so these never count.

Activity tab badge. One per subject: an order, an offer, or a message row. It counts only rows of these action types, only while their subject still needs this account:

Row type Counts while
return_updated, pickup_ready, payment_refund_required, bitcoin_manual_review The order it names is waiting on this account (the Orders rule above).
offer_received, offer_countered The offer is pending or countered, has not expired, and this account is the party who did not make the latest amount.
offer_accepted This account is the buyer and the award is still active, so checkout is still open.
message_received Always. There is no state that resolves it.
  • Unseen watch alerts on this device still count, one per alert.
  • Rows about an order or offer that failed to load do not count. The badge can lag, but it cannot invent work.
  • These rows never count: order_created ("Checkout started"), order_cancelled, order_cancelled_terms_change, payment_confirmed, payment_method_bound, fiat_payment_reported, order_shipped, order_delivered, order_delivery_assumed, order_completed, refund_recorded, review_received, outbid, auction_won, auction_ended, offer_rejected, pickup_details_updated, pickup_details_cleared, bitcoin_prepare_voided, drop_sold_out.

Marketplace icon (header and footer). This is the union of the Activity and Orders subjects. An order with a return row on Activity and a badge on Orders counts once.

Clearing works as before. Opening Activity clears Activity rows created before that moment, and opening Orders clears orders updated before that moment. A new event on something that still needs you raises the badge again. The sandbox keeps using each row's real readAt for Activity.

Per-account "seen" checkpoint

The durable service has no user-settings, last-seen, or read-state endpoint. Its notifications.read_at column exists, but no command writes it, and /v1/notification-preferences is sandbox-only. So the checkpoint lives in a small private homeserver file, following the private watchlist pattern:

pubky://<account>/priv/pubky.app/marketplace/v1/attention_seen.json = { "version": 1, "activitySeenAt": <ms>, "ordersSeenAt": <ms> }

  • Opening Activity or Orders raises this browser's copy first: Dexie for Activity, local storage for Orders. It then does GET, merges each side by maximum, and PUTs. On the first write, it also carries over any checkpoint this browser saved before this change.
  • Mounting a badge pulls the file and raises the local copies, so a view cleared in one browser clears in the others on their next page load. Only one pull per account runs at a time.
  • The checkpoints never move backward. A value saved by a device whose clock runs ahead is capped at this device's current time.
  • A file that fails its schema is never trusted and never overwritten.
  • The file is only read or written when all of these hold: the mode is durable, the owner is the signed-in account, and the session can write /priv/pubky.app/. Otherwise, such as a legacy public-only grant or the sandbox, the badge keeps its per-browser behaviour and makes no network calls.
  • The file sits under /priv/, so the timestamps are not public.

Changes

  • src/libs/commerce/marketplace-attention.ts: adds the actionable set. That covers orderNeedsCurrentUser (with the lapsed-hold rule), offerNeedsCurrentUser, activityNeedingAttentionKeys, and ordersNeedingAttentionKeys. It removes countOrdersNeedingAttention and markOrdersAttentionSeen, which no longer have callers.
  • New src/hooks/useMarketplaceActivityAttentionKeys and src/hooks/useMarketplaceOrdersAttentionKeys return subject keys. useMarketplaceActivityUnread and useMarketplaceOrdersAttention wrap them and return counts, so existing mocks and imports keep working. useMarketplaceNavAttention returns the size of the union.
  • New src/core/application/commerce/attention-seen.ts handles the private checkpoint file. It is wired through CommerceApplication and CommerceController (markActivityRead, markOrdersAttentionSeen, syncAttentionSeen). CommerceRecordNormalizer.attentionSeenUri builds the path.
  • New useMarkMarketplaceOrdersSeen hook. MarketplaceOrders now clears through the controller instead of writing local storage directly. A stubbed or missing controller counts as a failed write, not a render error.
  • The section-nav badge label changes from "N unread activity" to "N activity needing you".
  • Adds the changelog fragment activity-badge-actionable.fixed.md.

Tests (each fails when its change is reverted)

  • src/libs/commerce/marketplace-attention.test.ts: checks that "Checkout started", "Order cancelled", "Payment confirmed", and "Order completed" rows never count. Return and offer rows count only while their order or offer still waits on the account, and several rows about one subject count once. It also covers the lapsed-hold rule, offer turn-taking and expiry, and the accepted-offer award.
  • src/hooks/useMarketplaceActivityUnread/useMarketplaceActivityUnread.test.ts: an answered offer stops badging, order rows follow order state, and the hook pulls the account checkpoint.
  • src/hooks/useMarketplaceNavAttention/useMarketplaceNavAttention.test.tsx: an order that badges both tabs counts once on the icon. The existing identity-switch test is updated for the new reads.
  • src/core/application/commerce/attention-seen.test.ts covers:
    • a second browser with empty Dexie and local storage picks up Activity and Orders checkpoints saved by the first;
    • a pull tells mounted Orders badges to re-read;
    • the merge never moves backward;
    • a checkpoint from a clock that runs ahead is capped;
    • an unreadable file is never replaced;
    • no network calls without /priv write, for another account, or in the sandbox.

Six reverts were each run against these tests, and each one made at least one test fail:

  • order rows counting regardless of order state;
  • offer rows counting regardless of offer state;
  • order_created and order_cancelled added back to the action set;
  • the icon summing the two tabs instead of taking the union;
  • the checkpoint staying in one browser;
  • the hook never pulling.

Verification

  • npx vitest related --run --project unit <changed files> on the rebased head: 177 files, 2115 tests passed.
  • npm run typecheck, and eslint and prettier --check on the changed files, are clean.
  • The full Linux marketplace VRT suite ran in mcr.microsoft.com/playwright:v1.60.0-noble (scripts/vrt-linux.sh, every src/test/vrt/marketplace/*.vrt.test.tsx, Chromium and Firefox). 802 of 804 passed. The two failures were one Pickup scene in both browsers. Its mocked controller had no markOrdersAttentionSeen, and the new orders hook threw during render. The hook now treats a stubbed controller as a failed write, the same as the other badge hooks, and useMarkMarketplaceOrdersSeen.test.ts pins that behaviour; the test fails on the old hook. A re-run of the eight marketplace VRT files that render Orders, Activity, or the nav badges passed: 16 files, 168 tests.
  • No baselines were regenerated. No scene's pixels change: VRT pins the badge counts, and the only VRT edit is an aria-label assertion.

Known limits

  • Another browser picks up a clear on its next page load. There is no live push.
  • If two browsers write the file at the same moment, the lower checkpoint can win once. The next open corrects it.
  • Sessions whose grant lacks /priv/pubky.app/:rw keep per-browser clearing until the user signs in again with the wider grant.

…d per account

Activity rows badge only while their order or offer still waits on the signed-in account; informational rows never count, and one subject counts once on the marketplace icon. Opening Activity or Orders saves the checkpoint in the account's private homeserver document so every browser clears.
…wsers

Each save adds an immutable entry named by its checkpoint under a per-side private directory, and the checkpoint is the largest name. Saves are debounced, skipped when the homeserver already holds a newer checkpoint, and prune only entries below the one just written.
The Orders page saves when the list becomes ready, when the account changes, and when the tab becomes visible again. A refreshed order list no longer triggers a homeserver write.
@BitcoinErrorLog
BitcoinErrorLog merged commit 56ccc6e into release/shop-v0.6.8 Sep 24, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant