Skip to content

feat(announcements): show announcements in the notification surface - #890

Open
grunch wants to merge 4 commits into
feat/announcements-bridgefrom
feat/announcements-surface
Open

grunch wants to merge 4 commits into
feat/announcements-bridgefrom
feat/announcements-surface

Conversation

@grunch

@grunch grunch commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Linked issue

Part of #319 (step 4 of specs/006-announcement-channel/spec.md §10). Stacked on #889; the last PR of the stack closes the issue.

Type

feat

What changes and why

Rust's announcement snapshots become cards in Notifications. Each card is keyed by the announcement's address, so a correction (a new revision at the same address) replaces it unread, an address Rust no longer lists loses its card, and Rust's read flag wins. The banner paints the level with the Activity chips — info lime, warning amber, critical red — labels it (Announcement / Notice / Security update), shows the copy of the locale on screen, and offers the one link as a button opened externally; the body is never a link. A critical sorts above the timeline, turns the bell's dot red, and stays listed, read, once swiped away or on "Clear all". Nothing interrupts: no dialog, no snackbar, no takeover.

Affected flow

Notifications screen and the bell (lib/features/notifications/…, lib/shared/widgets/notification_bell.dart): tapping an announcement reads it and opens nothing; swiping dismisses it (a critical stays, read); its link button opens the browser. Startup (lib/core/app_bootstrap.dart) subscribes onAnnouncementsChanged before the relay pool starts and reads the cache once. No trade flow, no order status.

Blast radius

  • Sembast: announcement cards are ordinary notifications_v2 records (type: system, detail holds the revision, level, link and loc:<lang>:title|body); fromJson of every older record is unchanged. They bypass the processed_events ledger on purpose: Rust is the authority on what exists, and reconcileAnnouncements makes the store match each snapshot.
  • NotificationsNotifier.delete / deleteAll / _markRead learned about announcement cards; every other card takes the same path as before (notifications_*_test.dart, 197 tests, unchanged and green).
  • sectionNotices sorts a critical announcement first; every other entry keeps its newest-first order.
  • The bell shows the red dot for an unread critical as it does for the backup reminder; the count badge otherwise.
  • Localization: five new keys in all seven ARB files. No automation identifier, no wire change.
  • Web / desktop: url_launcher is already a dependency and used the same way by About.

Platforms tested

Not run on a device by the author of this diff. flutter analyze, flutter test, dart tool/design_check.dart --base feat/announcements-bridge (no breaks in the 7 files touched) on Linux.

Manual testing

Setup

Needs the whole stack built (this branch includes #889) and an allowlisted key, which only #892 adds — so run these on #892's branch, or temporarily add your own npub to ALLOWLIST in a local build. Publish with cargo run --bin announce (#891) and nak, to the default relays.

Steps

  1. Publish an info announcement with a link. Expected: the bell gets a count badge; Notifications shows a lime card labelled ANNOUNCEMENT with the title and body in the phone's language, and an "Open link" button.
  2. Tap the card. Expected: it turns read; nothing opens, no snackbar.
  3. Tap "Open link". Expected: the browser opens the URL.
  4. Switch the app language in Settings. Expected: the card's title and body follow.
  5. Re-publish the same id with a changed body. Expected: the card updates and is unread again.
  6. Publish a critical one. Expected: red card labelled SECURITY UPDATE, sorted first; the bell's dot is red. Swipe it away: it stays, read. "Clear all": it stays.
  7. Publish a warning with expires_in_days of a few minutes; wait past it; resume the app. Expected: the card is gone.
  8. Regression: a trade status change and a chat message still raise their cards as on main, and the backup-reminder style system banner looks as before.

Not covered

iOS, web; a locale this build does not ship.

Screenshots

Not taken (no device run). Design-guide rules touched: DS-COL-1, DS-COL-11 (the banner left AppColors for OrderBookPalette / ActivityPalette), DS-CMP-8 (radius 18, padding 14), DS-TYP-4 (10/11/12/13 on the scale).

Automated tests

test/features/notifications/announcement_cards_test.dart (16 tests): the card picks the locale's copy with English as fallback, is keyed by address with its revision and link, an unknown level paints as warning, each level has its label, a round trip through the store; a dismissed critical keeps its card and the other levels lose it; reconcile adds, removes and replaces (a correction is unread again, the same revision keeps the card, Rust's read flag wins); read and dismiss reach Rust, a swiped critical stays read, "Clear all" keeps a critical; sectionNotices sorts a critical first; the banner shows the level, the Spanish copy and the link, a plain system notice keeps its amber; the screen's tap reads and opens nothing; the bell turns red for a critical and counts an info.

flutter test: +3122 ~4 -2 (the two 10d_notifications_unsupported goldens, from #889's row, regenerated by the workflow).

Checklist

  • cargo …: no Rust change in this PR
  • flutter analyze and flutter test pass locally except the goldens above
  • Changed .arb: regenerated app_localizations*.dart committed in the same commit
  • New user-facing text: every locale updated, no hardcoded strings
  • Changed an automation identifier: n/a
  • Protocol or transport change: none
  • Behaviour change: spec §3.4 (the tones as built) and §6.2 ("What the surface settled")
  • Commits are signed
  • I ran the Manual testing steps myself — not yet

AI assistance

Written with Claude Code, the whole diff. Look hardest at NotificationsNotifier.reconcileAnnouncements / _dismissAnnouncement / deleteAll and at the banner's palette use.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 14 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 48db892 and 262e804.

Files selected for processing: 25
  • lib/core/app_bootstrap.dart
  • lib/features/notifications/models/notification_model.dart
  • lib/features/notifications/models/notification_view_rules.dart
  • lib/features/notifications/providers/notifications_provider.dart
  • lib/features/notifications/screens/notifications_screen.dart
  • lib/features/notifications/services/announcement_cards.dart
  • lib/features/notifications/widgets/system_notification_banner.dart
  • lib/l10n/app_de.arb
  • lib/l10n/app_en.arb
  • lib/l10n/app_es.arb
  • lib/l10n/app_fr.arb
  • lib/l10n/app_it.arb
  • lib/l10n/app_localizations.dart
  • lib/l10n/app_localizations_de.dart
  • lib/l10n/app_localizations_en.dart
  • lib/l10n/app_localizations_es.dart
  • lib/l10n/app_localizations_fr.dart
  • lib/l10n/app_localizations_it.dart
  • lib/l10n/app_localizations_nl.dart
  • lib/l10n/app_localizations_pt.dart
  • lib/l10n/app_nl.arb
  • lib/l10n/app_pt.arb
  • lib/shared/widgets/notification_bell.dart
  • specs/006-announcement-channel/spec.md
  • test/features/notifications/announcement_cards_test.dart
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T15:14:04.807096Z 262e804 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3a9b02f39

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +571 to +574
final gone = [
for (final n in state)
if (n.isAnnouncement && !wantedIds.contains(n.id)) n.id,
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reconcile against persisted announcements during initial hydration

On a cold start where an announcement expired while the app was closed, the first Rust snapshot can reach this method before loadInitialData() finishes. Because gone is computed only from the still-empty notifier state, the expired card is not deleted, and an empty snapshot returns immediately. The pending load then restores that card from Sembast, potentially including its unread critical badge, until another snapshot arrives. Coordinate reconciliation with hydration or reconcile against persisted records so cards absent from Rust's authoritative snapshot cannot be restored by the initial load.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 79179cd: the notifier keeps the addresses of Rust's last snapshot (_announced); loadInitialData skips every persisted announcement outside it and deletes those records, so a snapshot that lands before the load answers wins. Test a card Rust no longer lists is not brought back by a load in flight.

Comment on lines 217 to 218
recent.sort(criticalFirst);
return NoticeSections(needsAction: pinned, recent: recent);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Place critical announcements above the needs-action section

When a trade needs action, sectionNotices() puts it in needsAction, and NotificationsScreen._buildBody() renders that entire section before recent. Sorting critical announcements only within recent therefore leaves security updates below every actionable trade, potentially off-screen. This contradicts spec §3.4's requirement that critical announcements sort above everything, specifically so users see security warnings before continuing a trade. Render critical announcements ahead of both trade sections rather than only sorting the recent section.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 79179cd: NoticeSections has a critical section of its own, and the screen renders it before the needs-action section and the timeline. Test a critical announcement sits above a trade that needs action. Spec §6.2 updated.

@grunch

grunch commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Step 4 of specs/006 §10. Rust's snapshots become system cards keyed by
address: a correction replaces its card unread, a swept address loses
it, Rust's read flag wins. The banner paints the level with the Activity
chips (lime / amber / red), labels it, and offers the one link as a
button; the copy follows the locale on screen. A dismissed critical
stays listed, read, sorts first and turns the bell's dot red. Seven
locales of chrome.
…ust's snapshot

Review round 1 (#890).

- A critical announcement was sorted first within the timeline only, so a
  trade that needs action still rendered above it. It has a section of
  its own, rendered before both.
- Rust's first snapshot could land before the initial load answered, and
  the load then restored a card the snapshot no longer listed. The
  notifier keeps the addresses of the last snapshot; a load drops, and
  deletes, every persisted announcement outside it.
@grunch
grunch force-pushed the feat/announcements-surface branch from a3a9b02 to 79179cd Compare October 24, 2026 00:36
@grunch

grunch commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79179cd242

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +635 to +637
state =
byId.values.toList()
..sort((a, b) => b.timestamp.compareTo(a.timestamp));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Merge announcements into the state after persistence completes

On cold start, if loadInitialData() finishes while reconciliation is awaiting store.saveAll(changed), hydration adds the persisted trade and chat notices to state, but this assignment replaces them with byId, captured before those awaits. Loads run outside _mutationTail, so serializing mutations does not prevent this race. The existing notification history and its unread count disappear until another hydration or restart, even though the records remain on disk. Rebuild the map from the current state after persistence completes, then apply the announcement changes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ae00c90: after the store writes the merge starts from the state as it is then, not from the map captured before the awaits, so a hydration that landed meanwhile keeps its trade and chat notices. Test a load that lands while a snapshot is being persisted keeps its notices.

…rsisting

`reconcileAnnouncements` rebuilt the list from a map captured before its
store writes. Loads run outside the mutation queue, so a hydration that
finished during those writes had its trade and chat notices replaced by
that stale map until the next load or restart.

The merge now starts from the current state once the writes are done.
Test: a load that lands while a snapshot is being persisted keeps its
notices.
@grunch

grunch commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@grunch

grunch commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae00c90036

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +514 to +516
final card = state.where((n) => n.id == id).firstOrNull;
if (card != null && card.isAnnouncement) {
await _dismissAnnouncement(card);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Dismiss only the announcement revision that was swiped

If a correction arrives while the swipe's Undo snackbar is open, NotificationsScreen._dismiss still hides the address and later calls delete(id) when the snackbar closes. This lookup then selects the new revision rather than the one the user swiped, and _dismissAnnouncement reports that correction as dismissed to Rust. Non-critical corrections disappear without ever being shown; critical corrections lose their unread security badge. Capture the swiped revision and verify it before committing the delayed dismissal so a replacement remains unread and visible.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 262e804: _dismiss records the swiped announcement's revision and passes it to delete(id, announcementRevision:), which leaves a card whose revision differs alone — unread, nothing reported to Rust. Tests skips a correction that replaced the swiped revision and dismisses the revision that was swiped when it is still current.

@grunch

grunch commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…ped card

The Undo snack bar deletes by id when it closes. A correction that landed
meanwhile sits at the same address, so the delete found the new revision
and reported it dismissed to Rust: a non-critical correction vanished
unseen, a critical one lost its unread badge.

The screen now records the swiped revision and `delete` compares it with
the card's: a different one is left alone, unread, and Rust hears nothing.
@grunch

grunch commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: 262e804601

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@grunch

grunch commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@grunch

grunch commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@grunch grunch mentioned this pull request Oct 24, 2026
1 of 9 tasks

This branch has not been deployed

No deployments
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