* Filter blocked users and fix widgetbook analysis
* Filter blocked users across messaging surfaces
* Guard blocked-user loading windows
* Remove chat list subscription from chat screen and use chat summary (#633)
* chore: update rust crate
* refactor: move chat summary type to its own rust bridge file
* feat: add get chat summary to rust bridge
* refactor: add use chat summary hook
* feat: add util for shared logig on chat summary dispplay across screens
* refactor: remove chat list suscription from chat screen and replace use chat profile with chat summary
* refactor: use chat summary instead of chat profile in chat info screen
* refactor: use chat summary instead of chat profile in chat invite screen
* refactor: remove use chat profile hook
* Address blocked chat review feedback
* Preserve unread count for blocked chat updates
---------
Co-authored-by: Pepi <mariajosefinaalliende@gmail.com>
* fix(android): enable boot auto-restart for foreground service
The foreground service was not auto-restarting after device reboot,
so notifications only started working once the user reopened the
app. This was caused by two stacked misconfigurations:
1. `AndroidManifest.xml` explicitly disabled the plugin's
`RebootReceiver` and `RestartReceiver` via `tools:node="replace"`.
This was added in c3b4a1909 as defense-in-depth against an
upgrade-path crash caused by a `foregroundServiceType 0x201`
bitmask being persisted in native SharedPreferences. The
primary fix for that crash — a one-time prefs cleanup in
`MainActivity.onCreate()` — has been shipped for ~2 months,
so the defensive disable is no longer load-bearing.
2. `ForegroundTaskOptions` did not set `autoRunOnBoot` or
`autoRunOnMyPackageReplaced`, which default to `false`. Even
with the receivers enabled, the plugin's `RebootReceiver`
early-returns unless these flags are `true`.
Both fixes are required: the receiver is the *only* mechanism
the plugin provides for boot restart, and it only fires when
the Dart-side flags opt in.
Particularly impactful for GrapheneOS users, who typically run
without sandboxed Google Play Services and therefore cannot rely
on FCM wake-ups — the foreground service is their only path to
notifications.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: add headless-isolate spike to foreground task handler
Temporary instrumentation to verify the background isolate can reach
Rust (via flutter_rust_bridge), platform channels (path_provider), and
persisted data (accounts) when the foreground service starts.
Each step is logged with a `[SPIKE]` prefix so logcat can be filtered
with `adb logcat | grep SPIKE`. Also installs a log listener in the
task isolate's `_startCallback` — without one, log output never reaches
logcat because the main isolate's listener is scoped to its own isolate.
This will run on every foreground-service start, including:
- Normal app launch (main isolate already initialized whitenoise; spike
will log "already initialized" for that step)
- RebootReceiver auto-start (fresh isolate; spike should pass every step)
Comparing logcat between the two paths tells us whether a real headless
task handler is viable. To be removed when follow-up lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(android): initialize keyring context in Application.onCreate
The Rust keyring crate panics with "android context was not
initialized" when invoked from the foreground service after a
headless boot restart. Root cause: Keyring.initializeNdkContext was
called from MainActivity.configureFlutterEngine, which only runs
when an Activity is launched. When RebootReceiver starts the
foreground service directly, no Activity launches, so the JNI
Context pointer never gets handed to Rust.
Move the init to a new WhitenoiseApplication.onCreate. Application
subclasses run on every process start regardless of entry point
(Activity, Service, or Receiver), so the Context is ready for any
component that touches Rust.
Confirmed via device spike: all spike steps passed pre-reboot, and
initializeWhitenoise specifically failed post-reboot with this exact
panic. Re-running the spike after this change should show a full
"[SPIKE] DONE" line on the post-reboot path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(spike): log WhitenoiseApplication.onCreate to prove it runs
Spike still panics post-install with "android context was not
initialized" despite the WhitenoiseApplication fix. Two possibilities:
1. WhitenoiseApplication.onCreate isn't actually running in the
headless process (class-resolution or build-cache issue).
2. It is running, but Keyring.initializeNdkContext only sets up
keyring-internal state, not the global ndk_context that other
crates (rustls-platform-verifier-android, etc.) need.
Add an Android Log call so we can tell which case we're in. Also
wrap the Keyring call in try/catch so any failure there is visible
instead of silently allowing later crates to panic with a confusing
message.
After rebuilding with `flutter clean && flutter build apk --profile
--flavor staging --dart-define=WHITENOISE_ENABLE_SPIKE_LOGS=true`
and installing, logcat should include:
adb logcat | grep -E 'SPIKE|WhitenoiseApp'
If WhitenoiseApp lines appear → Application subclass loaded; problem
is a broader ndk_context init (H2). If they don't → class isn't being
loaded on this path (H1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor: extract NotificationSubscription for isolate-agnostic reuse
Previously the notification subscription lived inside
notification_provider.dart's _initializeAndListen and was gated on a
Riverpod Ref (so it could only run from the widget tree). This
prevents the foreground-task background isolate from reusing the same
subscription logic, which is what PR B needs.
Extract the subscription into lib/services/notification_subscription.dart
as a plain class. It takes dependencies via callbacks rather than Ref:
- ActiveChatGetter: String? Function()
- LocaleGetter: Locale Function()
The main-isolate provider wires these with ref.read(...). The upcoming
task handler will wire them with isolate-local defaults (no active
chat, locale from SharedPreferences).
Side effects of the extraction:
- formatNotification moves to the new file (still @visibleForTesting)
- handleNotificationUpdate becomes NotificationSubscription.handleUpdate
(tests migrate accordingly)
- notification_provider.dart shrinks; its coverage:ignore block now
wraps a smaller amount of genuinely-untestable Ref-wiring code
All ~40 existing tests for formatting + update handling migrate to
test/services/notification_subscription_test.dart. Coverage holds at
99%+. No behavioral change — this is a pure extraction.
Foundation for the next commit, which wires the new subscription into
the foreground-task handler for headless post-reboot delivery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(android): headless notification handler + isolate coordination
Replace the diagnostic SPIKE instrumentation with a real
_NotificationTaskHandler that runs inside the foreground-task
background isolate and owns the notification subscription when the
main UI isolate isn't running.
Behavior:
- On TaskStarter.system (boot / MY_PACKAGE_REPLACED, main isolate
not running): task isolate bootstraps Flutter bindings + Rust FFI
+ whitenoise, loads locale from persisted app settings, and starts
its own NotificationSubscription. Notifications arrive without
the user opening the app.
- On TaskStarter.developer (main isolate called foregroundService.
start()): task isolate bootstraps but does NOT start a subscription;
main owns it. Task awaits coordination via sendDataToTask.
- Coordination protocol (via FlutterForegroundTask.sendDataToTask):
{'event': 'main_started'} -> task stops its subscription
{'event': 'main_stopped'} -> task starts its subscription
The main-isolate caller triggers these via ForegroundService.
notifyMainStarted() / notifyMainStopped(). Wiring into the main
isolate's lifecycle observer lands in the next commit.
- onDestroy cancels the headless subscription so it doesn't leak
across service restarts.
- Headless notification tap currently just launches the app via
FlutterForegroundTask.launchApp(). Deep-link routing to the
specific chat/invite is tracked in #488.
Cleanup:
- Remove _runHeadlessSpike, _spikeShowTestNotification, and the
WHITENOISE_ENABLE_SPIKE_LOGS dart-define. Their job is done —
device testing confirmed all headless steps work.
- Remove SPIKE-reference comments from WhitenoiseApplication.kt.
Keep the try/catch + Log.e defensive around Keyring init.
- Logcat forwarding for ForegroundService + NotificationSubscription
loggers remains, gated on kDebugMode, for development visibility.
Tests:
- Existing ForegroundService tests remain green; new tests cover
notifyMainStarted/notifyMainStopped (sends correct event, no-op
when service isn't running, no-op when disabled).
- _NotificationTaskHandler itself runs in a separate isolate and
can't be unit-tested; its work (NotificationSubscription) is
fully covered in test/services/notification_subscription_test.dart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(android): send lifecycle signals from main to task handler
Wire _WnAppState as a WidgetsBindingObserver so the main isolate can
tell the task handler whether it owns the notification subscription:
- AppLifecycleState.resumed -> foregroundService.notifyMainStarted()
(task handler yields)
- paused/inactive/hidden/detached -> notifyMainStopped()
(task handler takes over notification delivery)
This closes the loop with the _NotificationTaskHandler coordination
protocol from the previous commit. Combined with the TaskStarter-based
initial decision, the notification channel is always owned by exactly
one isolate:
Boot/package-replaced (headless) -> task isolate owns it
Main isolate starts, pushes resumed -> main isolate owns it
Main isolate pauses/detaches -> task isolate takes over again
Edge cases accepted for this PR (tracked as follow-ups):
- If Android kills the main isolate without firing detached
(rare under a foreground service, but possible), the task handler
may remain idle. User reopen re-establishes ownership.
- Briefly during pause -> resume transitions, both isolates may
deliver the same update. NotificationService uses a group-id-
derived notification ID so the Android plugin de-duplicates
visually.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(android): deep-link handoff for headless notification taps
When the task isolate fires a notification and the user taps it, the
task handler now persists the payload (groupId, isInvite,
receiverPubkey) via FlutterForegroundTask.saveData before calling
launchApp(). The main isolate consumes the payload on startup /
resume and routes to the correct chat or invite screen, switching
the active account first if the tap targeted a different one.
Changes:
- foreground_service.dart:
- _persistTapAndLaunch: the task isolate's notification tap handler.
Stashes the payload, then launches the app.
- consumePendingNotificationTap(): public helper that reads and
clears the stashed payload. Returns a typed PendingNotificationTap
or null.
- notification_provider.dart:
- Rename _onNotificationTap -> handleNotificationTap (public).
- Refactor its signature to take plain values + a switchToProfile
callback instead of a Riverpod Ref, so it's callable from both
Provider (Ref) and ConsumerState (WidgetRef) contexts.
- main.dart:
- _WnAppState consumes pending taps in two places:
(a) post-frame callback from initState (covers first launch)
(b) AppLifecycleState.resumed (covers returning from background)
- Both paths guard against missing context / unmounted state.
Edge cases and trade-offs:
- In-app taps (main isolate active) still go through the synchronous
NotificationService callback; no SharedPreferences round-trip.
- If the user taps a headless notification but the router isn't yet
ready when we try to navigate, the logger warns ("No navigator
context") and the payload is dropped — next tap works. Acceptable
for first iteration; a deferred-navigation queue is a possible
follow-up if it surfaces in testing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: add unit tests for handleNotificationTap profile-switch logic
Covers three cases using plain-value callbacks (no Riverpod Ref
needed):
- Switches profile when the active pubkey differs from the tap's
target receiver
- Does NOT switch when they match
- Switches when there's no active pubkey (first-tap case)
TestWidgetsFlutterBinding.ensureInitialized() is required because
handleNotificationTap reaches Routes.navigatorKey.currentContext,
which touches the widget binding. The binding returns null in tests
(no router mounted), which the function tolerates via a warn log —
we're specifically testing the pre-navigation logic here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: restore 99.36% coverage baseline
CI flagged a coverage regression (99.36% → 99.07%) from the
headless-notification landings. Close the gap:
1. NotificationSubscription
- Add `enabled` parameter (like NotificationService) so tests can
bypass the Platform.isAndroid guard.
- Add tests covering start(), stop(), double-start idempotency,
stream event dispatch, stream errors, stream close, outer catch
on initialize failure, and default-enabled fallback.
- Narrow coverage:ignore on the defensive `if (_stopped)` branch
after stream.listen — structurally unreachable (no await between
the earlier _stopped check and this one) but kept as a guard in
case future edits introduce one.
2. ForegroundService
- Extract lifecycle switch into handleAppLifecycleChange method
so tests can drive each AppLifecycleState branch directly.
- Add tests for all five lifecycle states (resumed, paused,
inactive, hidden, detached) plus the disabled-service no-op case.
- main.dart's didChangeAppLifecycleState becomes a one-line call to
this method, drastically shrinking untestable widget-glue.
3. routePendingTap
- Extract the pending-tap routing logic from _WnAppState into a
top-level function in notification_provider.dart. Takes plain
values (bool isMounted, callbacks) — no Ref, no ConsumerState
dependency.
- Add tests covering null payload, unmounted guard, and the full
route-through case.
- main.dart now delegates to routePendingTap with an inlined bool
for mounted — no more uncovered lambdas.
4. Widget test for WnApp lifecycle
- Add a widget test that pumps WnApp and cycles through all
AppLifecycleState values. Verifies the observer chain wires
without throwing and exercises the main.dart switch body.
5. Re-export PendingNotificationTap + consumePendingNotificationTap
from notification_provider.dart so main.dart imports from a single
place.
Coverage: 99.07% → 99.36% (matches master baseline).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(android): claim subscription ownership + detect failed headless start
Two reviewer-flagged correctness gaps in the headless notification
delivery path:
1. Main isolate didn't claim ownership when opening after a system-
started service. ForegroundService.start() returns early when the
service is already running (post-reboot path), and
WidgetsBindingObserver doesn't replay the current lifecycle state
at registration — so notifyMainStarted() never fired before the
main isolate's NotificationSubscription started. Result: brief
double-subscription window where the task isolate (with
getActiveChatId: () => null) could surface notifications for the
currently-open chat.
Send notifyMainStarted() explicitly during _startForegroundAndSubscribe,
right after the service is known to be running, before the main
subscription starts.
2. Failed headless subscription start was recorded as success.
NotificationSubscription.start() catches its own startup failures
internally; _NotificationTaskHandler stored the wrapper before
awaiting start, then logged success regardless. A transient boot-
time failure left _subscription non-null forever, so future
coordination signals early-returned and the user got perma-silence
until service destruction.
Move the _subscription assignment after the await, check
sub.isRunning, and only persist the wrapper on actual attachment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add thumbhash support with blurhash fallback
Bump whitenoise-rs to 0d8024be and mdk-core to fbd3a1be to pick up
thumbhash generation alongside blurhash. Surface the new thumbhash
field through the Flutter Rust bridge, add the thumbhash Dart package,
and replace WnBlurhashPlaceholder with WnMediaPlaceholder that prefers
thumbhash, falls back to blurhash, then a neutral color.
Also wire the new LeftGroup ChatListUpdateTrigger variant from upstream.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: fix dart formatting in wn_media_error_placeholder
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: add CLAUDE.md symlink pointing to AGENTS.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add thumbhash and leftGroup test coverage
- WnMediaPlaceholder: thumbhash rendering, preference over blurhash,
invalid thumbhash fallback (already covered)
- WnMediaErrorPlaceholder: thumbhash display, preference over blurhash
- ChatMediaThumbnail: thumbhash placeholder while loading, preference
- ChatMessageMedia: thumbhash placeholder, preference, error state
- MediaImage: thumbhash placeholder, preference, error state,
removal after fade
- useChatList: leftGroup trigger removes chat, no-op for unknown chat
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: leftGroup trigger keeps chat in list; guard empty hash tags
- leftGroup now updates the chat item in the map (like removedFromGroup)
instead of removing it, so ChatScreen's isRemovedFromGroup check can
detect the removal and show the notice/disable input
- Guard thumbhash and blurhash imeta tag emission against empty/whitespace
strings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use trimmed hash values in imeta tag emission
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: filter logs by level
* chore: log timings for start chat related logic
* fix: dm chat profile fetches metadata with blocking sync if needed
* feat: move chat with support to settings screen
* feat: start chat screen shows avatar while loading key package and is following
* test: fix tests
* test: improve coverage
* fix: address PR review issues in app logs and start chat screens
- fix: hasFilters now accounts for level filtering in app logs screen
so the filtered count label appears when only level filters are active
- fix: remove no-op AnimatedOpacity wrapping CircularProgressIndicator
in start chat screen (opacity was hardcoded to 1, never animated)
- test: add missing coverage for helpState.isLoading guard in settings
screen chat with support menu item
---------
Co-authored-by: Jeff Gardner <202880+erskingardner@users.noreply.github.com>
* fix: blurhash size in message bubble media
* fix: show sender data in chat invite screen bubbles
* feat: add retry method to message service
* refactor: make chat status names match rust
* fix: padding and status positioning in message bubbles and add retry on tap
* feat: retry sending messages from chat screen
* docs: update changelog
* chore: updgrade whitenoise-rs and mdk-core
* refactor: remove unused animated pixel overlay widget
* feat: add base media placeholder widgets
* feat: add media thumbnail widget
* feat: add media download hook
* feat: add media display widgets
* feat: add media preview and modal widgets
* feat: add media upload support
* refactor: replace reply preview with chat message quote
* feat: integrate media into chat screen
* docs: update changelog
* test: replace magic number 844 with testDesignSize.height in WnTooltip test
* fix: remove thumbnail gradient overlay, center image in remaining space
* fix: constrain modal height so image fills remaining space below header
* test: improve coverage
* Refactor camera permission flow
* Update CHANGELOG.md
* chore: add permission handler
* feat: add scanner translations
* fix: request camera permission and use permission handler to go to settings in case of denied
* fix: show camera option in ios settings
---------
Co-authored-by: josefinalliende <mariajosefinaalliende@gmail.com>
* feat(auth): multi-step login with relay resolution UI
Replace the single-shot login API with a multi-step flow that gracefully
handles missing relay lists instead of crashing. When relay lists aren't
found on the network, users are now presented with a relay resolution
screen offering two paths: provide a custom relay URL to search, or
publish default relay lists.
- Update whitenoise-rs to multi-step login PR (3435fd9)
- Add LoginResult/LoginStatus/LoginError types to Rust API bridge
- Add login_start, login_publish_default_relays, login_with_custom_relay,
login_cancel bridge functions (nsec + external signer variants)
- Refactor AuthNotifier, hooks, and AndroidSignerService for multi-step flow
- Add RelayResolutionScreen with relay URL input and default relays option
- Add structured login error messages (invalid key, timeout, no connections)
- Remove obsolete AccountSettings bridge (removed upstream)
Closesmarmot-protocol/whitenoise#46, marmot-protocol/whitenoise#143
* fix(auth): standardize error keys, add translations, maximize test coverage
- Standardize all login hook errors to l10n keys instead of raw English
strings (paste errors, all 5 LoginError variants)
- Remove redundant useEffect cleanup from hooks (useTextEditingController
handles its own disposal)
- Wrap fire-and-forget userMetadata call with unawaited() for clarity
- Simplify relay resolution error resolver to two-branch switch
- Translate all 15 new l10n keys into DE, ES, FR, IT, PT, RU, TR
- Add 4 new l10n keys: loginErrorNoLoginInProgress, loginErrorInternal,
loginPasteNothingToPaste, loginPasteFailed
- Add tests for all uncovered provider, screen, hook, and service paths
Coverage: 99.43% overall, 100% on all changed files
* fix(auth): remove unused hook param, disable buttons during loading, assert loginCancel
- Remove unused isExternalSigner parameter from useRelayResolution hook
(the screen uses it to select callbacks, not the hook itself)
- Disable both relay resolution buttons while loading to prevent
concurrent requests
- Add assertion to loginCancel test verifying the Rust API receives
the correct pubkey
* fix(auth): restore nsec cleanup on dispose, use consistent error key
- Restore useEffect cleanup that clears nsec from TextEditingController
on widget dispose (security fix from defb482 accidentally removed)
- Replace orphaned 'relayResolutionPublishFailed' error key with
'loginErrorGeneric' for consistency with tryCustomRelay error handling
- Regenerate flutter_rust_bridge code after rebase onto master
* fix(auth): independent button loading, relay URL prefill and validation
- Split isLoading into isPublishingDefaults and isSearchingRelay so each
button shows its own loading indicator while the other is just disabled
- Prefill relay URL input with wss:// matching the network settings pattern
- Add debounced relay URL validation (shared utility extracted from
use_add_relay) with inline error display via WnInput.errorText
- Wrap fire-and-forget userMetadata call with error handling in
_completeLogin to prevent unhandled async exceptions
- Replace map-iteration-order-dependent testNpubToHex.values.first with
explicit testPubkeyA constant in mock
- Add mounted guard (useRef<bool>) to skip state updates after unmount
- Map caught ApiError variants to structured l10n keys instead of
hardcoding loginErrorGeneric; expand _resolveError to match
* test: add coverage for metadata fetch failure, validation reset, and external signer method routing
- Test that login completes even when fire-and-forget userMetadata fails
- Test that clearing relay URL back to wss:// prefix resets validation state
- Add invocation flags to mock auth notifier and assert external signer
tests call the correct methods (not the regular login methods)
* chore: add flutter local notifications and flutter foreground task deps
* feat: add bridge notifications methods
* feat: add foreground service to keep notifications stream alive
* feat: add notification service and provider
* feat: add provider to store active chat for notification purposes
* feat: add active chat provider to store if we are in a chat screen
* feat: add active chat route observer to clear active chat on navigation
* feat: add use active chat hook to cancel notifications on active chat
* feat: use active chat hook on invite and chat screen
* feat: translate notifications messages in notification provider
* fix: improve notification system reliability and reduce duplicate state tracking (#271)
* fix: improve notification system reliability and reduce duplicate state tracking
- Simplify useActiveChat hook: remove duplicate set/clear that overlaps
with ActiveChatRouteObserver, keep only lifecycle handling and
notification cancellation
- Fix provider modification during widget build: defer
ActiveChatRouteObserver state updates via Future.microtask
- Add error handling in _initializeAndListen with try/catch
- Wire up battery optimization exemption request after foreground
service start
- Replace non-deterministic String.hashCode with SHA-256-based
notification ID generation
- Remove unused RECEIVE_BOOT_COMPLETED permission from manifest
- Expand test coverage for disabled-mode foreground service,
notification service, and deterministic ID generation
* fix: restore active chat tracking in useActiveChat hook with deferred provider updates
The hook's setActiveChat/clearActiveChat calls were removed in the previous
commit, breaking notification clearing — users received notifications for
the chat they were actively viewing.
Restore the calls but defer them via Future.microtask() to avoid Riverpod's
'provider modification during build' error. Add ref.mounted guards to the
ActiveChatNotifier to handle disposal edge cases.
* feat: improve notifications for multiple accounts (#272)
* fix: improve notification system reliability and reduce duplicate state tracking
- Simplify useActiveChat hook: remove duplicate set/clear that overlaps
with ActiveChatRouteObserver, keep only lifecycle handling and
notification cancellation
- Fix provider modification during widget build: defer
ActiveChatRouteObserver state updates via Future.microtask
- Add error handling in _initializeAndListen with try/catch
- Wire up battery optimization exemption request after foreground
service start
- Replace non-deterministic String.hashCode with SHA-256-based
notification ID generation
- Remove unused RECEIVE_BOOT_COMPLETED permission from manifest
- Expand test coverage for disabled-mode foreground service,
notification service, and deterministic ID generation
* feat: show receiver name in notifications when multiple accounts are logged in
When more than one account exists, notification titles include the
receiver's display name in parentheses (e.g. 'Alice (MyAccount)') so
users know which account the notification is for. Single-account users
see no change.
Closes#265
* feat: switch to correct account when notification is tapped
Include receiver pubkey in the notification payload so the tap handler
can switch to the correct account before navigating to the chat or
invite screen. Exposes a navigator key from Routes so navigation can
happen outside the widget tree.
* fix: use fresh account count per notification and JSON-encode payloads
- Fetch accounts on each notification instead of capturing count once at
init, so multi-account display stays current when accounts change
- Replace pipe-delimited notification payload with JSON encoding to
avoid misparse when groupId contains '|' characters
- Add validation and error handling for malformed payloads
- Expand tests for JSON payload format and edge cases
* fix: address review feedback and upgrade flutter_local_notifications to v20
- Upgrade flutter_local_notifications ^19.0.0 → ^20.1.0 (named params)
- Upgrade crypto ^3.0.6 → ^3.0.7
- Bump Java compatibility to 17 for v20 compatibility
- Guard _initializeAndListen against provider disposal with ref.mounted
- Change _initializeAndListen to void return (fire-and-forget)
- Make Future.microtask timing consistent in useActiveChat hook
- Add _initialized warning in cancelForGroup for consistency
- Remove non-essential comments in onRepeatEvent
- Use valid 64-char hex pubkeys in tests
- Fix uninitService test to assert on its own mock
* fix: await show, validate payloads strictly, use realistic test pubkeys
- Await notificationService.show in _handleNotificationUpdate so errors
propagate to the stream's try/catch
- Wrap stream callback in try/catch to prevent unhandled async exceptions
- Reject unknown triggers and empty receiverPubkey in _handleNotificationTap
- Replace placeholder pubkeys (pk1/pk2) with valid 64-char hex strings
- Add tests for empty receiverPubkey and unknown trigger rejection
* test: improve notification coverage from 98.05% to 99.40%
- Add tests for handleNotificationUpdate (active chat skip, multi-account
receiver name, DM/group formatting, correct pubkey forwarding)
- Add tests for requestPermission (null plugin, granted, denied, null result)
- Add tests for foregroundServiceProvider and notificationServiceProvider
- Fix uninitService test to use its own mock
- Add coverage exclusions for platform-gated code that requires Android
runtime (providers, _initializeAndListen, _onNotificationTap,
_navigateToNotificationTarget, _KeepAliveTaskHandler, static
FlutterForegroundTask calls)
- Make handleNotificationUpdate @visibleForTesting for direct testing
---------
Co-authored-by: JeffG <202880+erskingardner@users.noreply.github.com>
* feat: add replying to state to chat input hook
* feat: add reply preview type
* feat: get reply preview in user chat messages hook
* feat: add reply preview widget
* feat: add optional reply preview to message bubble
* feat: add on reply callback to message actions screen
* feat: add reply tags support to message service
* feat: add reply support in chat screen
* chore: ignore coverage of reply preview typedef file cause lcov considers 0% because it has no executabel code, but it does have tests
* fix: author metadata fetch and rebuild
* feat: add translations for signer related messages
* refactor: add constants file with nostr events kinds
* refactor: move signer methods to service
* refactor: add nsec storage option to use nsec hook
* refactor: simplify auth provider for signer related methods
* refactor: signout screen with different callout msg depending on nsec storage
* refactor: show callout in profile keys screen for signer nsec storage
* refactor: rename use login hook to use login with nsec
* refactor: rename use android signer hook and make api more similar to the use login with nsec
* refactor: use in login screens renamed hooks and improve ux a bit
* refactor: remove unused android signer service provider
* docs: update docs files
* test: switch profile navigation
* chore: use whitenoise app team and bundle id
* chore: add apk commands
* chore: rename package from sloth to whitenoise
* docs: remove sloth word from readme and agents.md
* chore: replace sloth texts with whitenoise
* chore: remove unused error screen
* docs: copy LICENSE file from whitenoise repo
* chore: upgrade version to 0.3.0+15 so that android apk can replace old whitenoise ones
* refactor: rename MyApp to WnApp
* chore: use new staging bundle id
* chore: rename org.parres to IPF
* chore: remove .so file
* fix: make android signer work after bundle id change
* core: replace org.parres with dev.ipf for widgebtook
* test: improve coverage
* feat: update bridge to include user reaction id
* feat: add methods to delete reactions in message service
* feat: add translation for reaction error message
* feat: change message actions screen to handle reaction deletion
* feat: adjust message reactions for deletion tap
* feat: delete reactions in chat screen
* docs: update changelog
* feat: add send reaction method to message service
* feat: process reactions events
* feat: add widget to show reaction pills in message bubble
* feat: show message reactions in message bubble
* feat: add reaction logic to message menu
* feat: add on reaction callbacks in chat screen
* fix: change slate color
* refactor: remove redundant group id argument from message serivce methods
* docs: update changelog
* test: improve coverage of chatlist tile
* feat: handle message deletion event
* feat: hide deleted messages bubbles and add long press callback
* feat: add method to delete messages in message service
* feat: add message menu widget
* feat: show message menu with delete logic in chat screen
* docs: update changelog
* feat: use cached network image in avatar and unify them in single widget
* chore: ignore ai folders
* feat: add encoding util and move hex to npub conversion there
* feat: fetch user and rename service to user service
* feat: add hook to search user by npub
* feat: add search field component
* feat: add user search screen
* feat: add user search route
* docs: update changelog
* test: avoid mocks duplication and add misisng icon keys
* refactor: extract fade overlay to its own widget
* feat: fade overlay for user search results scroll
* refactor: extract repeated metadata name logic to util
* refactor: extract fetching useer metadata logic to service
* feat: use welcomer pubkey in chat list tile to show who invited you to the group
* feat: replace chat list pull to refresh with streams
* test: improve test by extracting repeated rust lib mock to a mock file
* test: improve coverage by adding screen test cases
* docs: update changelog
* test: find by key instead of specific icon
* feat: add hook for chat input
* feat: add message service to send text messages
* feat: store chat messages mapped by id and save latest message id
* feat: add hook to handle chat scroll
* feat: send messages from chat screen
* chore: update pubspec.lock
* docs: update CHANGELOG