Commit Graph
97 Commits
Author SHA1 Message Date
JeffGandGitHub d57a883e53 Implement iOS NSE notifications and Android push token plumbing (#673)
* Add remote push notification plumbing

* Checkpoint notification reliability cleanup

* Prepare notification reliability PR

* Fix notification PR review findings

* Address notification review follow-ups

* Clean up failed iOS data migration copies

* Address app resume and migration cleanup review

* Restore coverage for notification review fixes

* Fix App Group migration guard

* Address notification PR review feedback

* Cover notification review edge cases

* Address notification test review feedback

* Address NSE migration and startup retry review

* Use foreground catch-up for notification refresh

* Update whitenoise-rs foreground catch-up dependency

* Cover notification catch-up error paths

* Fix notification review edge cases
2026-05-20 14:47:09 +02:00
JeffGandGitHub 30ab65da28 [codex] Add key package developer controls (#685)
* Add key package developer controls

* Refactor bulk key package deletion hook

* Localize key package d-tag label

* Preserve bulk delete action on refresh failure
2026-05-20 12:21:45 +02:00
JeffGandGitHub 7c543c32a0 Add native deep links for users, chats, and settings (#661)
* Add copy deep link action to chat debug screen

* Address deep link review feedback

* Address deep link PR review follow-ups

* Scale profile QR code size
2026-05-15 12:34:58 +02:00
JeffGandGitHub e480f49e9e fix(android): external signer notification recovery (#635)
* Reconcile external signer callbacks on auth lifecycle

* Fix headless external signer notification recovery

* Point whitenoise-rs dependency at merged recovery fix

* Address external signer notification review feedback

* Address notification review follow-ups

* Cover external signer registry fallback paths

* Use developer log in foreground task
2026-05-11 17:21:50 +02:00
JeffGandGitHub 22bd445a85 Fix system notice dismiss after unmount (#646) 2026-05-11 08:52:22 +02:00
JeffGandGitHub a4fec570d9 Make group info screen scrollable (#648) 2026-05-10 14:35:10 +02:00
JeffGandGitHub 5a246d1ced fix: create android release checksums (#634) 2026-05-07 15:52:04 +02:00
Jeff Gardner f77d85db44 chore: prepare v2026.5.7+24 release 2026-05-07 13:46:17 +02:00
bed6630303 Filter blocked users across Flutter messaging surfaces (#623)
* 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>
2026-05-07 11:07:31 +02:00
JeffGandGitHub 931cec376a [codex] Bump whitenoise-rs master and refresh Android assets (#617)
* Bump whitenoise-rs master and refresh Android assets

* Update version to 2026.5.4

* Use shared group chat flag in chat screen
2026-05-05 17:46:22 +02:00
JeffGandGitHub b100fc1ec5 Add Fastlane release scaffolding (#601)
* Add Fastlane release scaffolding

* chore: document release flow and tag guard

* fix: address release automation review comments

* fix: harden release version parsing

* fix: resolve release review follow-ups

* fix: restore Android staging application id
2026-04-29 22:34:19 +02:00
92b7e9b205 fix(android): enable boot auto-restart for foreground service (#577)
* 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>
2026-04-29 02:32:57 +02:00
JeffGandGitHub 733bc72742 Update whitenoise-rs master ref (#566) 2026-04-13 22:51:56 +02:00
fa9112c90b feat: add thumbhash support with blurhash fallback (#549)
* 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>
2026-04-04 09:30:13 +02:00
JeffGandGitHub f1174d9d2f feat: update whitenoise-rs and add MIP-05 push registration bridge (#548) 2026-04-03 03:08:08 -03:00
JeffGandGitHub ed948bd099 Remove unused PR labeler workflow (#543) 2026-03-27 10:38:24 +01:00
JeffGandGitHub f871105cae Update android keystore settings (#540)
* Update android keystore settings

* Configure separate staging Android signing

* Fix staging signing config generation

* Fix Android APK workflow signing setup
2026-03-26 18:52:59 +01:00
JeffGandGitHub 5f7f0512c0 chore: add mobile push notification prerequisites (#539)
* chore: add mobile push notification prerequisites

* fix: address push prereq review feedback
2026-03-26 15:40:25 +01:00
JeffGandGitHub 4a1674d7ae chore: prepare 2026.3.23+22 release (#529) 2026-03-23 12:36:57 +01:00
JeffGandGitHub 8714625f13 Stream user metadata updates (#522)
* feat: stream user metadata updates

* fix: address review feedback
2026-03-20 17:36:38 +01:00
JeffGandGitHub 2dd6a1ca19 The great relay restoration (#495)
* Update relay tooling and developer settings

* Update whtienoise-rs version

* Fix reviewed relay diagnostics and locale issues
2026-03-11 00:13:08 -03:00
72b68941b3 chore: fix production build flags and remove redundant just recipes (#467)
* chore: fix production build flags and remove redundant just recipes

- Add --dart-define=APP_FLAVOR=production to Android build in
  build_release.sh (was missing, causing debug features to be enabled
  in production Android builds due to defaultValue: 'staging')
- Remove fat APK recipes (build-android-apk, build-production-apk,
  build-staging-apk, when-apk) — project always uses split APKs
- Remove build-release-apk and build-release-aab aliases — use
  build-split-apk and build-aab directly

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* chore: restore when-apk recipe pointing to split APK staging build

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-07 18:02:56 +01:00
Jeff Gardner ad7dc1d751 Bump build number and remove debug from production iOS build 2026-03-05 17:12:51 -03:00
JeffGandGitHub ffe3e87d16 chore: bump whitenoise-rs to 0.2.1 and prep 2026.3.5 release (#450)
* chore: bump whitenoise-rs to 0.2.1 for 2026.3.5 release

* chore: sync widgetbook lockfile with app version

* fix: export iOS deployment target for Rust iOS build

* chore: align changelog release key with app version

* chore: drop build suffix from changelog release key
2026-03-05 19:38:34 +01:00
JeffGandGitHub 0793026b52 chore: bump whitenoise-rs to 0.2.0 and align mdk rev (#444) 2026-03-05 16:04:28 +01:00
JeffGandGitHub 1716ddd56b Add zapstore assets and fix changelog (#379) 2026-02-23 14:40:44 +01:00
JeffGandGitHub 07a9060e9a Add release builder (#378) 2026-02-23 14:40:17 +01:00
JeffGandGitHub 6500d844aa Bump whitenoise-rs version (#376) 2026-02-23 13:33:57 +01:00
JeffGandGitHub ec18bcce2c Add version to settings footer, bump whitenoise crate rev (#367)
* Add version to settings footer, bump whitenoise crate rev

* Update icons and get staging iOS updated for TestFlight

* fix: surface group operation errors to the user

- GroupInfoScreen: show error notice when member list fails to load
- SetUpGroupScreen: show warning notice when group image upload fails
  (group is created successfully but user is informed the image didn't upload)
- EditGroupScreen: show save failure as a notice and keep Save/Cancel
  buttons visible so the user can retry, rather than replacing the form

Refactors useCreateGroup.createGroup to return ({Group? group, bool
imageUploadFailed}) instead of Group?, making partial success explicit
in the return value rather than as a state side-effect.

* fix(l10n): translate groupImageUploadFailed into all supported locales
2026-02-23 10:49:23 +01:00
JeffGandGitHub ae203e9d65 Update iOS icons and app name for staging (#330)
* Update iOS icons and app name for staging

* fix(ios): harden build script and correct Release display name

- Add cargo check to environment validation in build_ios.sh
- Add existence check before cd rust to give informative errors
- Replace fragile cd/pod install/cd with pushd/popd and error handling
- Add POSIX-compliant trailing newline to build_ios.sh
- Fix project-level Release APP_DISPLAY_NAME ("White Noise staging" → "White Noise") to match target-level value

* feat(android): add staging flavor launcher icons

Add ic_launcher_foreground, ic_launcher_background, ic_launcher_monochrome,
and legacy ic_launcher PNGs for all densities (mdpi through xxxhdpi) plus
the mipmap-anydpi-v26 adaptive icon XML in the staging source set.

* fix(ios): add CocoaPods availability check to build script

* chore: replace LICENSE copies with symlinks to root

Fixes missing license warnings during iOS builds by ensuring
rust/ and rust_builder/ both symlink to the root LICENSE file.
2026-02-20 19:50:04 +01:00
JeffGandGitHub 373bb829c6 Fix OS back button closing app from chat screen (#353)
The chat screen is navigated to via GoRouter.go() (stack replacement),
leaving it as the root of the navigation stack with nothing to pop back
to. The in-app back button correctly called Routes.goToChatList(), but
the Android OS back button had no handler and defaulted to Navigator.pop()
on a root route, closing the app.

Wrap ChatScreen in PopScope(canPop: false) so the OS back gesture is
intercepted and redirected to goToChatList() instead.

Fixes #334
2026-02-20 19:49:24 +01:00
JeffGandGitHub e919e7bbeb fix: make dropdowns mutually exclusive and reduce transition duration (#337)
* fix: make dropdowns mutually exclusive using WnDropdownScope

Adds WnDropdownController and WnDropdownScope to WnDropdownSelector,
following the same pattern as WnListItemController/WnListItemScope.
When multiple WnDropdownSelector widgets are wrapped in a WnDropdownScope,
opening one automatically closes any other that is open.

Updates AppearanceScreen to use WnDropdownScope so the theme and language
dropdowns cannot both be open at the same time (fixes #332).

* fix: reduce slate content transition duration from 250ms to 150ms

* fix: set slate content transition duration to 200ms

* test: achieve 100% coverage on all files touched by this branch

- Add WnDropdownController unit tests (openItemKey getter, close no-op,
  open same key no-notify, controller lifecycle)
- Add WnDropdownSelector tests covering controller integration paths:
  toggle-closes via controller, selectOption via controller, disable-
  while-open with controller, non-ValueKey effectiveKey fallback
- Add use_dropdown_controller_test.dart for the new hook
- Add AppearanceScreen tests verifying exclusive dropdown behaviour
- Add WnSlateContentTransition duration assertion (200ms)
- Fix controller.close() called during build by deferring via
  addPostFrameCallback

* fix: revert incorrect Spanish translation change

The precommit l10n auto-fix incorrectly dropped the 'a' before
{userName} in addToGroupConfirmation. Restored to the correct
'¿Añadir a {userName} a {groupName}?' in both the ARB source and
generated file.
2026-02-20 17:56:59 +01:00
JeffGandGitHub c3b4a19098 fix: handle app upgrade crashes from incompatible data and foreground service receivers (#312)
* fix: handle app upgrade crashes from incompatible data and foreground service

When upgrading from an older version, multiple issues caused crashes:

1. The Rust whitenoise crate's secrets store format changed, causing
   "Secrets store error: Key not found" during initialization. Since
   the Rust singleton (tokio::sync::OnceCell) can't be reinitialized
   after partial failure, a version marker file now detects stale data
   and wipes the data directory before Rust init runs.

2. The flutter_foreground_task plugin stored foregroundServiceType
   0x201 (dataSync|shortService) in native SharedPreferences, but the
   manifest only declares dataSync (0x01). Android rejected
   startForeground() with IllegalArgumentException. A one-time cleanup
   in MainActivity.onCreate() clears these native prefs.

3. FlutterSecureStorage's internal migration from EncryptedSharedPreferences
   re-introduced the old active_account_pubkey after deleteAll(). Fixed by
   calling readAll() first to trigger the migration, then deleteAll().

4. The plugin's RebootReceiver and RestartReceiver caused cascade crashes
   on package update. Both are now disabled via manifest merge overrides.

* chore: remove stale url_launcher from generated plugin files and add build improvements

Remove url_launcher references from Linux/Windows plugin registrants
(dependency was already removed from pubspec.yaml). Add split APK,
AAB build targets and quiet spinner to OpenSSL build script.
2026-02-19 14:47:51 +01:00
JeffGandGitHub 0e4641888f Remove chat filter pills from search header (#319)
The Chats/Archive filter chips in the pull-down search header are not
hooked up to anything and won't be before release. Remove them from
the widget tree and reduce the header height accordingly. The
WnFilterChip widget itself is kept for future use.
2026-02-19 14:18:01 +01:00
JeffGandGitHub 7b795ab2c1 Improve signup name UX and compact add-profile slate (#313)
* Improve signup name UX and compact add-profile slate

* Add PR reference to changelog entry

* Upgrade unique_names_generator to v3
2026-02-18 12:52:12 +01:00
JeffGandGitHub 6c830c8eb1 chore(ci): add PR labeler and improve coverage reporting (#304)
* chore(ci): add PR labeler and improve coverage reporting

* chore(ci): split lint test and coverage jobs

* fix(ci): dedupe coverage history entries by sha

* fix(ci): use github.workspace in cache paths

* fix(ci): run flutter tests once for coverage

* fix(ci): consume coverage from shared test artifact

* fix(ci): align coverage guidance and add checkout

* fix(ci): checkout before downloading coverage artifact

* chore(coderabbit): dedupe title ignore keywords

* docs: refine widget naming conventions into three categories

Encode reviewer feedback distinguishing design system widgets (Wn prefix),
complex reusable widgets (no prefix), and screen-scoped widgets (screen
name prefix) in both .coderabbit.yaml and AGENTS.md.

* fix(ci): cache lcov apt package to avoid repeated installs
2026-02-18 10:04:20 +01:00
JeffGandGitHub 3666e69ca3 fix: add timeout and loading state to delete all data flow (#306)
* fix: add timeout and loading state to delete all data flow (#257)

The delete all data operation could hang forever with no feedback if the
Rust call never completed. Additionally, the confirmation overlay would
dismiss before the operation started, causing a visual jump back to the
privacy screen during loading.

- Add 30-second timeout to deleteAllData hook to prevent infinite loading
- Show loading state on the confirmation button instead of popping back
- Disable cancel/back/dismiss while operation is in progress
- Add onConfirmAsync support to WnConfirmationSlate for async operations

* fix: reduce delete all data timeout to 10 seconds

* refactor: remove dead sync branch from WnConfirmationSlate.show()

Make onConfirmAsync required since all callers pass it, removing the
unreachable sync SafeArea path and duplicate tests that exercised it.
2026-02-18 09:20:23 +01:00
JeffGandGitHub 44d7795734 Implement chat info screen redesign from Figma (#303)
* Implement chat info sheet redesign and interactions

* Add PR link to chat info changelog entry

* Localize chat info labels for non-English locales

* Address chat info widget review cleanup

* Use follow/unfollow copy in chat info actions
2026-02-18 08:25:19 +01:00
JeffGandGitHub f2d202b4f3 fix: resolve sender metadata for push notifications when displayName is missing (#305)
* fix: resolve sender metadata for push notifications when displayName is missing (#282)

When the Rust crate's NotificationUpdate lacks a sender displayName
(common for new invites), fetch metadata via UserService before
formatting the notification. Falls back to 'Unknown user' if the
fetch fails or returns no name fields.

* fix: use distinct values in senderName precedence test to verify actual precedence
2026-02-17 20:51:27 +01:00
JeffGandGitHub a4147776d5 feat: add 40px variant to WnInputFieldButton (#285)
* feat: add 40px variant to WnInputFieldButton (#276)

Introduce WnInputFieldButtonSize enum with size36, size40, and size48
variants, each encoding both dimension and icon size. This replaces the
previous WnInputSize-based sizing on WnInputFieldButton with a dedicated
buttonSize parameter, enabling the new 40px button for 56px input fields.

* refactor: extract WnInputFieldButton, internalize inline action sizing

Address PR #285 review feedback:

- Extract WnInputFieldButton and WnInputFieldButtonSize into
  dedicated wn_input_field_button.dart file
- Add `filled` parameter (default true) supporting transparent variant
  used by WnInputPassword and WnCopyableField
- Replace WnInput's `inlineAction: Widget?` with `inlineActionIcon`,
  `inlineActionOnPressed`, and `inlineActionFilled` so the input
  manages button sizing internally based on WnInputSize
- Refactor WnInputPassword inline actions to use WnInputFieldButton
  instead of manual GestureDetector/Container
- Add WnInputSize.inlineActionButtonSize getter to centralize
  size44→size36, size56→size48 mapping
- Update widgetbook with filled/unfilled showcase and knobs
2026-02-17 11:56:41 +01:00
JeffGandGitHub ca8d6a87bf feat: add WnUserItem component (#284)
* feat: add WnUserItem component (#279)

Simple row widget displaying a user avatar, name, and optional label.
Includes 100% test coverage and widgetbook showcase entry.

* feat: add image variant to WnUserItem widgetbook showcase

* feat: add size variants (small/medium/big) with checkbox and npub support

Redesigns WnUserItem with three Figma-matched sizes:
- Small: avatar(xSmall) + name + optional label
- Medium: avatar(small) + name + npub(WnMiddleEllipsisText, 2 lines) + checkbox
- Big: avatar(medium) + name + npub(WnMiddleEllipsisText, 2 lines) + checkbox, fixed height

Adds onTap, isSelected, showCheckbox props. 28 tests, 100% coverage.

* fix: use minHeight instead of fixed height on big variant

The fixed height of 76px was clipping the second line of npub text.
Using minHeight allows the container to grow to fit content.
2026-02-17 11:56:27 +01:00
JeffGandGitHub 147b6bccde feat(auth): multi-step login with relay resolution UI (#281)
* 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)

Closes marmot-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)
2026-02-17 11:41:56 +01:00
JeffGandGitHub 5aea549f6e fix: remove manual scroll-to-bottom on signup screen keyboard open (#287)
Remove the useEffect that scrolled to maxScrollExtent when the keyboard
appeared, which was hiding the name input field. Flutter's Scaffold with
resizeToAvoidBottomInset (default true) and built-in focus management
already handle scrolling the focused field into view natively.

Closes #144
2026-02-17 08:17:46 +01:00
JeffGandGitHub 815be13c43 refactor(theme): rename Fill Content/Destructive to Fill Content/Quaternary (#283)
* refactor(theme): rename fillContentDestructive to fillContentQuaternary

This token is now used beyond destructive contexts, so the old name
was misleading. Rename to fillContentQuaternary to match the existing
primary/secondary/tertiary naming convention.

Closes #274

* style: break long fillContentQuaternary lerp line into multiline
2026-02-16 19:13:38 +01:00
JeffGandGitHub cc4f1f8fe7 feat(settings): Privacy & security screen, rename to Appearance, update scroll effects (#254)
* feat(settings): add Privacy & security screen, rename App Settings to Appearance, update scroll edge effects

- Create Privacy & security screen with delete-all-data functionality (closes #247)
- Rename App Settings to Appearance, remove delete-all-data from it (closes #248)
- Add separator and secondary menu items for Donate/Developer settings
- Update scroll edge effect heights (slate: 40px, canvas: 196px) with smoother 3-stop gradient
- Add localization for all new strings across 8 languages
- Full test coverage for new and updated screens

* fix: title-case deleteAllAppData l10n keys, extract MockAuthNotifier to shared helper

* fix(l10n): use lowercase verb in German deleteAllAppData key

* fix(privacy): match Figma design for confirmation dialog and section labels

- WnConfirmationSlate: position at top of screen (not bottom), stack buttons vertically with 8px gap
- Privacy screen: section label uses semiBold16 with secondary color (grey) per design
- Confirmation dialog: use action-specific title/message/button text instead of generic
- Add deleteAllAppDataConfirmation and deleteAllAppDataWarning l10n keys across all 8 languages

* fix(confirmation): use back button instead of close X in confirmation slate
2026-02-14 17:34:29 +01:00
JeffGandGitHub feaf2177e8 feat(login): improve Amber login UX and fix disabled button opacity (#250)
* feat(login): improve Amber login UX and fix disabled button opacity

Rename "Login with Signer" to "Login with Amber" for clarity across
all locales. Disable the nsec login button when the field is empty
instead of keeping it always enabled. The Amber button is now always
outline style rather than swapping primary/outline based on input state.

Fix disabled button rendering to match Figma: apply 0.25 opacity to
the entire button container instead of individual color channels. The
previous per-color alpha approach made text nearly invisible in light
mode. This affects all button types (primary, outline, ghost, overlay,
destructive).

Add `just run` convenience command for running with flavor.

Closes #191

* chore: add Eclipse/Android Studio project files to gitignore

* chore: fix gitignore comment and add .classpath to Eclipse entries

* fix(button): remove disabled opacity — render disabled buttons at full opacity

* style(button): apply prefer_final_locals lint fix
2026-02-13 15:17:49 +01:00
JeffGandGitHub defb48234e fix(security): clear clipboard after copying private key and clean up nsec references (#251)
* fix(security): clear clipboard after copying private key and clean up nsec references (#29)

Add useClipboardGuard hook that clears the system clipboard 60 seconds
after copying the private key, reducing the window where the nsec is
accessible to other apps. Also clear the TextEditingController in
useLoginWithNsec on dispose to remove the nsec from the controller
promptly.

* fix: await Clipboard.setData in timer callback and add screen-level clipboard guard tests

Make the Timer callback async so exceptions from the Clipboard.setData
Future are properly caught by the existing try/catch. Add integration
tests to both ProfileKeysScreen and SignOutScreen verifying the clipboard
is cleared 60 seconds after copying the private key.

* fix: persist clipboard guard timer across screen navigation

Use a module-level timer instead of a hook-scoped ref so the clipboard
is cleared even after navigating away from the keys/sign-out screen.
Previously the timer was cancelled on widget dispose, meaning the
clipboard would only be cleared if the user stayed on the screen for
the full 60 seconds.

Addresses review feedback from untreu2.

* fix: resolve lint errors in clipboard guard imports

* test: add clipboard failure test for 100% coverage on touched files

* test: add assertion in clipboard failure test and dispose ValueNotifier
2026-02-13 15:09:44 +01:00
JeffGandGitHub 49fbc7ad27 feat(chat-list): search filtering in chat list (#243)
* feat(chat-list): add search filtering to chat list

Wire up the existing search bar in the chat list to filter chats by
name. For DMs this matches on the peer's display name, for groups on
the group name. Filtering is case-insensitive substring matching.

- Add filterChatsBySearch utility function
- Connect WnSearchAndFilters onSearchChanged to filter state in ChatListScreen
- Use controller listener in WnSearchAndFilters for reliable callback delivery
- Add unit tests for filterChatsBySearch and integration tests for screen

Closes #241

* fix(chat-list): keep search bar visible when search yields no results

When a search query matched zero chats, WnChatList early-returned with
a static empty state, replacing the scrollable list and header overlay.
The user could not access the search bar to change their query.

Now WnChatList keeps the full Stack layout when isSearchActive is true,
shows a 'No results' message in the content area, and forces the header
open so the search field remains accessible.

* fix(chat-list): add hasHeader to useEffect dependency array

The effect that forces the header open when search is active reads
hasHeader but only depended on isSearchActive. If header/headerHeight
changed while isSearchActive was already true the effect would not
re-run.
2026-02-13 08:00:56 +01:00
JeffGandGitHub 415eb0c0cc fix(android): build static OpenSSL for Android and initialize keyring-core (#245)
* fix(android): build static OpenSSL for Android and initialize keyring-core

Android doesn't ship a system libcrypto.so, which SQLCipher (via
libsqlite3-sys) requires. This adds a build script that cross-compiles
OpenSSL as a static library for each Android target (aarch64, armv7,
x86_64) and points the Rust build at those per-target installations.

Also adds the Keyring Kotlin class and initializes the NDK context
in MainActivity so keyring-core can access Android's secure storage.

* fix(android): verify OpenSSL tarball SHA-256 integrity after download

The SHA-256 hash was defined but never checked, and the value itself
was truncated (40 chars, not 64). Replace with the correct hash from
the official release and add verification before extracting.

* fix(android): add pipefail and remove tail pipes in OpenSSL build script

set -e alone does not catch failures in pipeline commands. Adding pipefail
ensures configure/make errors propagate instead of being masked by | tail -3.

* fix(android): detect Apple Silicon NDK path and portable SHA-256 check

The Darwin case now inspects uname -m to set HOST_TAG to darwin-arm64 on
Apple Silicon instead of always using darwin-x86_64.

SHA-256 verification falls back from shasum to sha256sum so the script
works on Linux systems that only ship sha256sum. Both tools use the same
output format (hash followed by filename) so awk '{print $1}' works for
either.
2026-02-12 18:14:58 +01:00
JeffGandGitHub d09ac69501 fix(deps): update bytes and time to patch security vulnerabilities (#246) 2026-02-12 17:45:22 +01:00
JeffGandGitHub bb33016aff feat(user-search): search by name with improved UX and metadata handling (#234)
* feat(user-search): search by name with improved UX and metadata handling

- Redesign user list tile to match profile switcher pattern (medium avatar,
  middle-ellipsis npub, consistent padding)
- Sort follows: users with metadata alphabetically first, then without
- Periodically refresh follows to pick up background metadata updates
- Batch name search stream updates (300ms) to reduce list jerkiness
- Add loading spinner in search field during active name search
- Sanitize malformed UTF-16 in metadata (surrogate pairs + UTF-8 round-trip)
- Pass metadata to start chat screen for instant display
- Truncate long about text to 10 lines in user profile card

* docs: update changelog with PR #234

* fix: screenutil height unit, improve test specificity and coverage

- Fix SizedBox height in search loading indicator to use .h instead of .w
- Target specific about Text widget in truncation test via content match
- Add tests for stream onDone, onError, and whitespace query edge cases
- Coverage improved from 99.31% to 99.41%

* refactor: remove unreachable else branch in useUserSearch

The isNameQuery check always evaluates true at that point since
_isNameSearch returns true for any non-empty query that isn't a hex
pubkey or partial npub — exactly the cases remaining after the
preceding branches.

* fix: log errors in useUserSearch onError instead of swallowing them
2026-02-12 15:51:51 +01:00
7fb6c94ac7 feat(theme): add Reaction semantic colors (#236)
* feat(theme): add Reaction semantic colors (#218)

Add ReactionColorSet and SemanticReactionColors to the theme system
with incoming/outgoing fill, hover, selected, and content color tokens
for both light and dark modes. Also adds Neutral/250 and Neutral/700
to the base palette.

* docs: add reaaction colors to widgetbook story (#240)

---------

Co-authored-by: Pepi <mariajosefinaalliende@gmail.com>
2026-02-12 15:13:15 +01:00
JeffGandGitHub 0b99e4fc03 feat(avatar): add xSmall (36px) avatar size variant (#215) (#237) 2026-02-12 14:49:44 +01:00
JeffGandGitHub 854538dca7 feat(theme): add 16px and 12px compact typography styles (#238)
Add compact variants for 16px (line-height 18px) and 12px (line-height
13px) with all three weight variants (medium, semiBold, bold). These
match the regular styles but with tighter line heights for compact
layouts.

Closes #214
2026-02-12 14:48:10 +01:00
JeffGandGitHub c8b8addc67 feat(theme): add Overlay Tertiary semantic color (#239)
Add overlayTertiary to SemanticColors, set to BlackAlpha(500) for both
light and dark themes. Closes #213
2026-02-12 14:46:44 +01:00
JeffGandGitHub 21c52783a1 fix: lock app orientation to portrait mode (#235)
* fix: lock app orientation to portrait mode (#233)

Disable device rotation since there is no landscape design.
Lock orientation at both Flutter (SystemChrome) and native
(iOS Info.plist, Android manifest) levels.

* fix: consistent indentation for orientation plist entries
2026-02-12 12:54:23 +01:00
JeffGandGitHub 2352bad48b fix: wipe old unencrypted database on upgrade (#224)
* fix: wipe old unencrypted database on upgrade

When initializeWhitenoise() fails with 'database was created without
encryption', delete the env-specific data directory (data/dev or
data/release) and retry initialization to create a fresh encrypted
database. Unrelated errors are rethrown without wiping.

Closes #222

* refactor: extract error string to constant and fix async test assertions

Extract 'database was created without encryption' into a shared
kUnencryptedDatabaseError constant in main.dart so tests import and
reuse the same value. Fix two async tests that were not awaiting the
Future from initializeAppContainer() by using expectLater.
2026-02-11 17:58:53 +01:00
JeffGandGitHub 322d28ae31 feat(chat-list): add long-press context menu with pin/unpin support (#211)
* feat(chat-list): add long-press context menu with pin/unpin support

Add a context menu overlay triggered by long-pressing a chat list item.
The menu shows a blurred preview of the chat item with action buttons
for Pin/Unpin, Mute, Archive, and Delete. Pin/Unpin is fully wired to
setChatPinOrder and refreshes the chat list after toggling. Mute,
Archive, and Delete are rendered as placeholders for future implementation.

- New WnChatListContextMenu widget with blur backdrop and fade/scale animation
- WnChatListItem gains onLongPress support
- ChatListTile wires long-press to show context menu for non-pending chats
- useChatList exposes refresh callback to re-subscribe the stream
- Localized pin/unpin/mute/archive strings across all 8 languages
- Full test coverage for context menu, tile integration, and refresh

* fix: address PR review feedback for context menu

- Fix Italian l10n: change 'unpin' from ambiguous 'Rimuovi' to 'Sblocca'
- Add error handling to setChatPinOrder: wrap in try/catch, only refresh
  on success, show error via WnSystemNotice on failure
- Add stable 'id' field to WnChatListContextMenuAction for locale-safe
  widget keys instead of deriving keys from localized label strings
- Refactor static overlay state to instance-scoped controller: replace
  static _currentEntry/_animatedDismiss/_dismissing with a
  WnChatListContextMenuController that scopes state per menu instance
- Add failedToPinChat l10n string across all 8 languages
- Add tests for pin error handling and onChatListChanged not called on failure

* fix(l10n): use 'Rimuovi' for Italian unpin translation

* fix: address second round of PR review feedback

- Rename _entry to _overlay in WnChatListContextMenuController for clarity
- Remove dead no-op static dismiss() method
- Replace private _ContextMenuButton with existing WnButton widget
  (outline type for normal actions, destructive type for delete)

* fix: make BoxShadow offsets ScreenUtil-responsive and improve test coverage

Use Offset(0.w, 1.h) instead of literal Offset(0, 1) in BoxShadow
instances. Add tests for context menu controller, card tap passthrough,
chat list item hover state, and chat list header scroll edge cases to
bring context menu and chat list item to 100% coverage.

* fix: remove border from destructive button and add no-op handlers to stub actions

Remove the border from WnButtonType.destructive to match the Figma
design (solid red background, no border). Add no-op onTap handlers to
the mute, archive, and delete context menu actions so they render in
their active state rather than appearing disabled.
2026-02-11 15:03:36 +01:00
JeffGandGitHub 4b158bf3f0 feat: Pinned chats display (#178) (#207)
* feat: add pinned chats display with pin badge on avatar

Pinned chats now sort to the top of the chat list ordered by pinOrder.
The WnAvatar medium variant shows a pin badge overlay at bottom-right
when showPinned is true. Also fixes the edit button icon size (16->24px)
and removes its incorrect border to match Figma specs.

Closes #178

* fix(tests): use valid 64-char hex pubkey in useChatList tests

Replace invalid 'pk1' literal with testPubkeyA from test_helpers.dart
to follow the project's pubkey guideline for test data.

* refactor: add const constructor to _PinBadge widget

Enables const folding and satisfies prefer_const_constructors lint.

* fix: remove Flutter-side pin sorting, trust Rust ordering

The Rust API already returns chats in the correct order (pinned first
by pin_order, then unpinned by activity). Sorting on every stream event
in Flutter was unnecessary overhead. Pin ordering is Rust's responsibility.

* fix: use backgroundContentSecondary for pin badge icon color

* fix: correct pin badge icon color to backgroundContentSecondary

Previous commit accidentally changed the edit button icon color instead
of the pin badge. This reverts the edit button back to
backgroundContentPrimary and applies backgroundContentSecondary to the
pin badge icon.

* fix: reduce pin badge icon to 16px inside 18px container

The 18px backgroundSecondary circle with a 16px icon creates a visible
1px ring around the pin icon, matching the Figma design.
2026-02-10 10:29:48 +01:00
e3be0184b1 feat(chat-list): add pull-to-reveal search and filters header (#195)
* feat: Add WnChatListItem and refactor ChatListTile to use it

- Create WnChatListItem widget matching Figma specs
- Refactor ChatListTile to use WnChatListItem
- Add "you" localization key
- Add tests for WnChatListItem and update ChatListTile tests
- Update test_helpers to support provider overrides

* fix(widgets): improve WnChatListItem design to match Figma

- Use medium14Compact typography for message preview (was medium14)
- Support 2-line message preview with ellipsis truncation
- Center status icon vertically relative to message text
- Add colored status icons: red for failed, blue for request
- Add widgetbook showcase with 'Is From You' knob
- Add two-line message examples to widgetbook
- Wrap widgetbook in DesignWidthContainer (390px)

* feat(widgets): add WnChatList container with scroll edge effect

Extract reusable WnChatList widget from ChatListScreen that handles
loading, empty, and list states. The chat list extends full-screen
behind the WnSlate header using a Stack layout, with a scroll-aware
gradient fade effect at the top so items visually dissolve as they
scroll up behind the slate and into the status bar area.

* feat(chat-list): add pull-to-reveal search and filters header

Add search/filter bar that slides down from behind the slate when
pulling down on the chat list. The header commits to open once the
user drags past 50% and dismisses when scrolling up. Includes
WnSearchAndFilters widget with search field and filter chips.

Closes #148

* add scroll animations for chat list

* fix(test): address PR review feedback and improve widget test coverage

- Rename misleading test description to 'renders chat status widget when status is provided'
- Remove weak 'renders selected state background' test that asserted nothing meaningful
- Add test for pending group with name to cover chat_list_tile.dart avatarName path
- Clean up unnecessary comments in wn_chat_list_item_test.dart

* fix(test): improve test style and use byKey for icon lookup

- Extract finder variable in chat_list_tile_test to keep lines under 100 chars
- Add Key('notification_off_icon') to WnChatListItem notification icon
- Replace widget predicate with find.byKey in notification off test
- Reformat long testWidgets declaration to wrap under 100 chars
- Remove unused wn_icon.dart import from test

* fix: translate l10n strings, remove unused isTyping param, fix test pubkeys

- Translate search/filterChats/filterArchive into de/es/fr/it/pt/ru/tr
- Remove unused isTyping parameter from WnChatListItem
- Use valid 64-char hex pubkeys in chat_list_tile tests
- Remove unnecessary comments from ChatListTile

* fix: restore reversed chat list ordering and extract magic constants

Restore reversed indexing in ChatListScreen itemBuilder so newest/updated
chats appear at the top, aligning with useChatList hook behavior.

Extract repeated values in WnChatList into named constants:
- _kAnimationDuration for the four Duration(milliseconds: 200) usages
- horizontalPadding for the duplicated 10.w ListView/header padding
- _kDismissThreshold (0.7) and _kOpenThreshold (0.5) for header reveal

Fix test pubkeys in chat_list_tile_test to use valid 64-char hex constants
and update chat_list_screen_test expectations for reversed ordering.

---------

Co-authored-by: emir yorulmaz <emiryorulmaz@ikmail.com>
2026-02-07 13:20:06 +01:00
a8c65d4a91 feat(overlay): add overlaySecondary color and WnOverlay variants (#187)
* feat(overlay): add overlaySecondary color and WnOverlay variants

- Add overlaySecondary semantic color (WhiteAlpha/500 for light, BlackAlpha/500 for dark)
- Add WnOverlayVariant enum with heavy (sigma 40) and light (sigma 10) variants
- Heavy variant uses overlayPrimary, light variant uses overlaySecondary
- Update widgetbook with realistic chat UI demo and toggle controls

Closes #153, closes #154

* fix: make overlay blur sigma responsive and fix quote style

- Use flutter_screenutil .r extension for _sigmaX/_sigmaY getters in WnOverlay
- Convert double-quoted strings to single quotes in widgetbook structure.dart

* Review overlay (#192)

* fix: change overlay primary color in dark mode to match figma

* docs: add overlay colors to sementic colors story

* fix: correct blackAlpha50 color value and minor code quality improvements

- Update blackAlpha50 from 0x08 to 0x0D (~5% alpha) to match naming convention
- Fix WnOverlay test to explicitly pass heavy variant instead of relying on default
- Wrap long description string in widgetbook to respect 100-char line width

* test: remove redundant WnOverlay heavy variant test

The test was redundant since WnOverlayVariant.heavy is the default value,
and passing it explicitly triggers avoid_redundant_argument_values lint.

* fix: add LC_ALL=C to l10n validation script for consistent sorting

Ensures consistent sort order across different environments/locales.

* fix: rename 'strong' to 'heavy' in WnOverlay test description for consistency

---------

Co-authored-by: Pepi <mariajosefinaalliende@gmail.com>
2026-02-06 12:19:52 +01:00
JeffGandGitHub d01e480dfa feat(widgets): add WnProfileSwitcherItem component (#188)
* feat(widgets): add WnProfileSwitcherItem component

- Create reusable profile switcher item for switch profile screen
- Use WnAvatarSize.medium (56px) per Figma specs
- Use WnMiddleEllipsisText for npub display with middle truncation
- Add widgetbook showcase with real npub examples
- Refactor SwitchProfileScreen to use new component

Closes #141

* fix: rename sloth package imports to whitenoise and fix l10n validation

- Update wn_profile_switcher_item imports from sloth to whitenoise
- Update test and widgetbook imports from sloth to whitenoise
- Fix validate-locales-keys.sh to use LC_ALL=C for consistent sorting
- Include auto-generated plugin registrant updates

* refactor: compute formatted pubkey once in WnProfileSwitcherItem build

Extract formatPublicKey(npubFromHex(pubkey) ?? pubkey) into a local
final variable to avoid duplicate computation in the build method.
2026-02-06 11:54:47 +01:00
JeffGandGitHub 113b4fb87c feat(widgets): add WnCarouselIndicator component (#186)
* feat(widgets): add WnCarouselIndicator component

Implement carousel indicator widget for showing position in paginated content.

- Active dot expands (28px) with primary fill color
- Inactive dots are circular (8px) with secondary fill color
- Directional bounce animation using elasticOut curve (600ms)
- Fast color transition (150ms) for smooth deactivation
- 8px gap between indicator dots

Closes #103

* fix: use .r for border radius and remove context anti-pattern

- Change BorderRadius.circular(4.h) to BorderRadius.circular(4.r)
- Remove stored BuildContext field in _InteractiveCarouselIndicator
2026-02-06 11:22:30 +01:00
JeffGandGitHub c49410b577 feat(widgets): add WnFilterChip component (#189)
* feat(widgets): add WnFilterChip component

Add a new filter chip component for filtering chat lists with support
for All, Unread, Archived, Favorites, and user-defined groups.

- Two variants: standard (in-flow) and elevated (sticky filter bar with shadow)
- Three states: default, hover, and active (selected)
- Includes comprehensive tests with 100% coverage
- Adds widgetbook entry showcasing all variants and states

Closes #152

* refactor(widgets): convert WnFilterChip to HookWidget and apply screenutil scaling

- Replace StatefulWidget with HookWidget using useState for hover state
- Inline context.typographyScaled to remove unused typography variable
- Apply .r scaling to blurRadius and spreadRadius in BoxShadow

* test(widgets): fix hover exit test to achieve 100% coverage

Move pointer to Offset(-100, -100) instead of Offset.zero to ensure
the mouse truly exits the widget bounds and triggers onExit callback.
2026-02-05 14:41:14 +01:00
JeffGandGitHub 6c0c747e2f feat(widgets): add WnChatStatus component (#190)
* feat(widgets): add WnChatStatus component (#147)

Add chat status indicator component for chat list items showing
delivery state (sent, delivered, read), error states (failed),
pending requests, and unread message counts with fixed-size badges.

* fix(widgets): correct ScreenUtil extents and improve test stability in WnChatStatus

- Use .h for height values and .r for icon sizes per ScreenUtil guidelines
- Make width variable final using switch expression (prefer_final_locals)
- Add Key to WnIcon and use find.byKey in tests instead of find.byType
2026-02-05 14:25:00 +01:00
JeffGandGitHub ca4c4065d0 chore: stop tracking compiled Rust .so files (#184)
Remove jniLibs from git tracking and add to .gitignore.
These are build artifacts generated by `just build-android`.

This prevents future repo bloat (~34 MB per architecture per commit).
Historical bloat will be cleaned up in a coordinated history rewrite.
2026-02-05 09:34:02 +01:00
7ee642533a feat(widgets): add WnKeyPackageCard component (#173)
* feat(widgets): add WnKeyPackageCard component

Add a reusable key package card component for displaying key package
details in developer settings. The card shows the package title, ID,
created timestamp, and a destructive delete button.

- Create WnKeyPackageCard widget with full test coverage
- Add component to widgetbook
- Refactor developer_settings_screen to use the new component

Closes #105

* feat(developer-settings): add scroll edge effects to key packages list

* fix(widgets): use localized string for delete button in WnKeyPackageCard

Replace hardcoded 'Delete' text with context.l10n.delete to support
internationalization across all 8 supported locales.

* refactor(widgets): clean up WnKeyPackageCard build methods and make onDelete non-nullable

- Inline context.typographyScaled in _buildTitleRow, _buildIdField, and _buildCreatedAtField
- Remove unused typography local variables to keep build methods lean
- Change onDelete from VoidCallback? to VoidCallback (non-nullable)
- Remove test case for null onDelete since it's no longer valid

* refactor(widgets): pass deleteLabel as prop to WnKeyPackageCard

Remove l10n dependency from WnKeyPackageCard by accepting deleteLabel
as a required parameter. This allows the widget to be used in widgetbook
without requiring localization setup.

* test: improve coverage of developer settings screen

---------

Co-authored-by: josefinalliende <mariajosefinaalliende@gmail.com>
2026-02-05 08:17:15 +01:00
07b32b47ed feat: add WnTimestamp component (#171)
* feat: add WnTimestamp component (#149)

Add a timestamp widget for chat list that displays relative/absolute time
based on message age:
- Now: < 60 seconds
- Minutes: 1-59m (e.g., "32m")
- Hours: 1-12h (e.g., "4h")
- Time: same day, 12+ hours (e.g., "14:30")
- Yesterday: previous day, 12+ hours ago
- Weekday: 2-6 days ago (e.g., "Monday")
- Date: > 6 days, < 1 year (e.g., "Jun 10")
- Date & Year: > 1 year (e.g., "Jan 9, 2009")

Includes localization for 8 languages and widgetbook showcase with
interactive date/time picker playground.

* refactor: extract _monthShortName helper to reduce duplication

* fix(l10n): use clear Turkish minutes abbreviation 'dk' instead of ambiguous 'd'

* refactor: remove unused context parameter from _InteractiveTimestamp

* fix(timestamp): use calendar-day logic for weekday branch

Replace duration-based difference.inDays with normalized date comparison
to fix misclassification of calendar-day offsets near midnight.

* fix(timestamp): use duration-based check for one-year boundary

Replace date normalization with currentTime.difference(date).inDays <= 365
to handle leap year Feb 29 correctly and ensure consistent behavior across
all years. Add test for leap year edge case.

* refactor(wn_timestamp): inline typography variable

Remove local typography variable and use context.typographyScaled
directly in the Text style.

* Custom datetime knob (#177)

* feat: add custom datetime knob for stories

* refactor: move timestamp story controls to knob

---------

Co-authored-by: Pepi <mariajosefinaalliende@gmail.com>
2026-02-04 19:34:15 +01:00
JeffGandGitHub 2cae365f69 feat(wn_input): add optional leading icon support (#172)
* feat(wn_input): add optional leading icon support

Add leadingIcon parameter to WnInput widget that displays an icon
on the left side of the input field, matching the Figma design specs.

Closes #151

* fix(wn_input): use .h for height in leading icon SizedBox

Change height from 16.w to 16.h to follow responsive sizing guidelines.

* fix(wn_input): block leading icon interactions when disabled

Wrap leadingIcon in IgnorePointer to prevent taps/clicks when the
input field is disabled, consistent with inlineAction and trailingAction.

* fix(wn_input): use .r scaling for leading icon padding and size

Change leading icon wrapper padding and SizedBox dimensions from
.w/.h to .r for consistent radius-based scaling.
2026-02-04 16:17:52 +01:00
8e18dbce40 feat(theme): add centralized AppTypography system (#170)
* feat(theme): add centralized AppTypography system

- Add AppTypography ThemeExtension with all font size/weight variants
- Replace inline TextStyle definitions across screens and widgets
- Use context.typographyScaled for responsive font sizing
- Update wn_list_item and wn_tooltip to use new typography system
- Reduces code duplication and ensures consistent typography

* chore: remove comments from AppTypography

* refactor: inline single-use typography variables

Remove unnecessary typography variable declarations in files where
context.typographyScaled is only called once. Direct inline access
is cleaner for single-use cases.

* refactor(theme): remove fontFamily overrides and add size 10 typography

- Remove explicit fontFamily: 'Manrope' from all 42 TextStyle variants
  in AppTypography to allow theme-level font changes to propagate
- Add medium10, semiBold10, bold10 typography variants (fontSize: 10,
  height: 14/10, letterSpacing: 0.8)
- Remove redundant Builder widgets in WnSystemNotice and hoist
  typography lookup to build method scope
- Replace one-off fontSize: 10.sp overrides in WnMessageReactions
  with typography.medium10
- Add widget-level tests for AppTypographyExtension in theme_test.dart
- Add tests for size 10 typography styles

* fix(theme): scale letterSpacing in _scaleStyle for responsive typography

Apply .sp scaling to letterSpacing in addition to fontSize to ensure
letter spacing scales responsively across different device sizes.

* feat(widgetbook): add typography showcase to foundations

Display all 48 text style variants organized by size (96-10) with
Medium, SemiBold, and Bold weight variants. Each section shows specs
(font size, line height, letter spacing) and sample text.

* feat: replace missing TextStyle with typography

---------

Co-authored-by: josefinalliende <mariajosefinaalliende@gmail.com>
2026-02-04 14:57:55 +01:00
JeffGandGitHub a79a039218 feat: add addFilled icon variant (#169) 2026-02-04 11:02:44 +01:00
JeffGandGitHub db8404a6a3 feat: add WnList component (#159)
* feat: add WnList component with widgetbook story (#139)

- Add WnList and WnListItem widgets with title/subtitle support
- Support leading/trailing widgets, selection state, separators
- Add comprehensive test suite with 100% coverage (26 tests)
- Add widgetbook story with interactive playground

* refactor(widgets): split list components and align with Figma design

- Split WnListItem into separate file (wn_list_item.dart)
- Update WnList to use children parameter (matching menu pattern)
- Align ListItem styling exactly with Figma specs:
  - Fixed 44px height with 8px border radius
  - Asymmetric padding (14px left, 6px right)
  - 4px gap between items instead of separators
  - Leading icon 20px, trailing icon 18px
  - Text: 14sp Medium, 0.4sp letter spacing
- Add 100% test coverage for both components
- Update widgetbook examples

* fix(widgetbook): add actions to Interactive States list demo

The second list item 'Tap the menu to expand' was missing the
actions property, so no expandable menu was displayed. Added
Edit and Delete actions to match the described behavior.

* test(widgets): add 100% coverage for WnListItem interactive states

Add tests for pressed state and tap cancel callbacks to achieve 100%
test coverage for both WnList and WnListItem components.

Also fix height values to use .h instead of .w for proper responsive sizing.

* refactor(widgets): convert WnListItem from StatefulWidget to HookWidget

Replace StatefulWidget with HookWidget and useState hooks for ephemeral
state (_isExpanded, _isPressed) per AGENTS.md guidance.

Also:
- Use actions! directly in buildExpandedActions since hasActions guard
  ensures non-null
- Update test to use find.byKey for menu icon instead of find.byIcon

* fix(widgets): address PR review feedback for WnListItem

- Replace Material icons with SVG icons (warningFilled, checkmarkFilled,
  errorFilled, more) for consistency with design system
- Change leadingIcon from showIcon bool to WnIcons? parameter, allowing
  custom icons for neutral type items
- Replace custom action buttons with WnButton component
- Add close button to collapse expanded actions menu
- Refactor NetworkScreen to use WnList/WnListItem for relay lists
- Remove WnRelayTile widget (replaced by WnListItem)
- Update widgetbook examples and tests

* feat(widgets): close expanded list item menu on tap outside or scroll

- Add WnListItemController and WnListItemScope for managing expanded state
- Only one list item can be expanded at a time within a scope
- Tapping any list item collapses the expanded menu
- Scrolling collapses the expanded menu
- Remove X close icon - always show 'more' icon
- Add useListItemController hook for lifecycle management
- Use unique keys per category in NetworkScreen to prevent collisions

* fix(widgets): hide more icon when list item menu is expanded

* fix(widgets): remove extra right padding from expanded list item actions

* fix(widgets): improve WnListItem effectiveKey calculation and add vertical padding

- Fix effectiveKey to prefer any widget key (not just ValueKey) before
  falling back to title, preventing potential key collisions
- Add debug assertion requiring explicit itemKey when using UniqueKey
- Add 6.h vertical padding above and below expanded action buttons
2026-02-04 10:41:56 +01:00
JeffGandGitHub 2b6e329855 feat: add WnTooltip component (#160)
* feat: add WnTooltip component with widgetbook story (#142)

- Add WnTooltip widget with 4 positions (top, bottom, left, right)
- Support hover and long-press to show, tap outside to dismiss
- Configurable arrow visibility and wait duration
- Add comprehensive test suite with 99% coverage (16 tests)
- Add widgetbook story with interactive playground

* fix(tooltip): correct typography, sizing, and dismissal behavior

- Update font size from 12.sp to 14.sp and add letterSpacing: 0.4.sp per Figma
- Fix _getOffset to use .h for heights and .w for widths (proper scaling)
- Add wasDismissed flag to prevent tooltip re-opening after outside tap
- Add setUpTestView(tester) to all tooltip tests for stable positioning

* fix(tooltip): align with Figma design

- Remove left/right positions (only top/bottom per spec)
- Remove showArrow param (tail always visible per spec)
- Fix colors: fillPrimary background, fillContentPrimary text
- Fix padding: 8px outer + 8px inner text container
- Fix text style: Manrope Medium 14/18, letter-spacing 0.4
- Update AGENTS.md with cleaner screenutil examples and 95% coverage

* fix(tooltip): use responsive letterSpacing and improve test coverage

- Update letterSpacing from 0.4 to 0.4.sp for responsive scaling
- Add tests for mouse exit behavior (onExit callback)
- Add unit tests for ArrowPainter.shouldRepaint method
- Make ArrowPainter @visibleForTesting to enable direct testing
- Update AGENTS.md to clarify .sp usage for letter spacing
- Achieve 100% test coverage for wn_tooltip.dart

* fix(tooltip): use symmetric EdgeInsets for proper responsive spacing

Replace EdgeInsets.all(8.w) with EdgeInsets.symmetric(horizontal: 8.w,
vertical: 8.h) in _buildContentBox() for both Container padding and
inner Padding widget, ensuring vertical spacing uses .h while
horizontal uses .w per project conventions.

* fix(tooltip): add screen-edge awareness and improve UX

- Add screen boundary detection to prevent tooltip overflow
- Center tooltip on screen when it would overflow edges
- Keep arrow pointing at target element when tooltip shifts
- Add triggerMode parameter (tap/longPress) with tap as default
- Add max-width constraint based on screen width with padding
- Add fade-in animation to prevent visible position shifting
- Fix re-tap behavior so tooltip can be shown again after dismissal
- Update network screen to use WnTooltip instead of Flutter Tooltip

* fix(tooltip): position top tooltip below to avoid covering title

First section tooltip (My Relays) uses bottom position to avoid
covering the screen title, while other section tooltips use top
position as default.

* feat(tooltip): add left/right positions, showArrow option, and refactor to HookWidget

- Add left and right variants to WnTooltipPosition enum
- Add showArrow boolean option (default true) to control arrow visibility
- Convert _TooltipOverlay from StatefulWidget to HookWidget for cleaner lifecycle
- Fix static _horizontalPadding to compute dynamically in build()
- Add mounted guard using useRef to prevent setState after dispose
- Update ArrowPainter to draw arrows for all four directions
- Add comprehensive tests for new positions and showArrow option

* fix(tooltip): reset dismissedWhileHovering on tap/long-press gestures

On touch devices, onExit isn't called after tap-outside dismissal, causing
dismissedWhileHovering to stay true and block re-show via gestures.

- Reset dismissedWhileHovering before showTooltip in GestureDetector handlers
- Add tests for tap/long-press reopening tooltip after dismiss
- Refactor network_screen_test to use keyed icon pattern for tooltip assertions

* fix(tooltip): use correct screenutil scaling and always wire onLongPress

- Split padding into horizontalPadding (.w) and verticalPadding (.h) for
  proper responsive scaling in calculateShift overflow checks
- Always wire onLongPress so long-press shows tooltip regardless of triggerMode
- Add test verifying long-press works even when triggerMode is tap

* fix(tooltip): use correct screenutil scaling for arrow offset in getOffset

Use arrowHeight (.h) for top/bottom positions and arrowWidth (.w) for
left/right positions to ensure proper responsive scaling.
2026-02-04 09:38:06 +01:00
JeffGandGitHub e90e98eabc feat: add WnSpinner component (#158)
* feat: add WnSpinner component with widgetbook story (#138)

- Add WnSpinner widget with small/medium/large sizes
- Support optional label text and custom colors
- Add comprehensive test suite (100% coverage)
- Add widgetbook story with interactive playground

* Only type, not size

* refactor(widgets): convert WnSpinner from StatefulWidget to HookWidget

Replace manual AnimationController lifecycle management with
useAnimationController hook to follow repo's hooks-based pattern.

* refactor(widgets): use dart:math pi constant in WnSpinner

* refactor(widgets): replace CircularProgressIndicator with CustomPaint in WnSpinner

Implement custom spinner painting with track and arc design using
_SpinnerPainter. Each spinner type now uses distinct track/arc color
combinations from semantic colors.

* test(widgets): improve WnSpinner tests with real assertions and 100% coverage

- Fix animation test to verify rotation changes via Matrix4 extraction
- Fix linear easing test to assert consistent rotation deltas
- Add semantic label to spinner widget for accessibility
- Add tests for SpinnerPainter.shouldRepaint to achieve 100% coverage
- Expose SpinnerPainter with @visibleForTesting for direct unit testing

* Remove unneeded conditionals and drop tests that required private method

* fix(widgets): improve WnSpinner shouldRepaint and clean up tests

- Update shouldRepaint to compare arcColor and strokeWidth in addition
  to trackColor so painter repaints when any property changes
- Remove duplicate 'renders primary type' test
- Fix size test to use RenderBox via keyed WnSpinner instead of
  SizedBox.first which could match other widgets
2026-02-03 20:46:01 +01:00
JeffGandGitHub 8452780d7a Remove local dependency comments, add .vscode to gitgitnore, add bug fixes note to AGENTS.md (#156) 2026-02-02 17:08:12 +01:00
JeffGandGitHub 325f752de1 feat: add WnSlate, WnSlateHeader, and WnSlateHeaderAction components (#128)
* feat: add WnSlate, WnSlateHeader, and WnSlateHeaderAction components

Implement Slate component system based on Figma designs for issue #67:
- WnSlate: Main slate container with Hero animation for screen transitions,
  header variants (default/close/back/noHeader), scroll edge effects
- WnSlateHeader: Header with avatar, title, and action buttons
- WnSlateHeaderAction: Action buttons (newChat, close, back)

Includes comprehensive tests with 100% coverage.

* fix: use .sp for letterSpacing in WnSlateHeader

* refactor(WnSlate): accept header as widget prop instead of specific callbacks

Simplifies WnSlate API by accepting a header widget directly rather than
managing header-specific props (type, title, avatarUrl, callbacks).

Before:
  WnSlate(
    type: WnSlateType.close,
    title: 'Settings',
    onCloseTap: () => ...,
  )

After:
  WnSlate(
    header: WnSlateHeader(
      type: WnSlateHeaderType.close,
      title: 'Settings',
      onCloseTap: () => ...,
    ),
  )

This makes WnSlate a simple container with optional header slot,
giving callers full control over header configuration.

* fix(WnSlateHeader): remove hard-coded fontFamily to inherit from theme

* fix(WnSlateHeader): only apply title padding when action button is rendered

Previously, padding was based solely on action type (back vs close),
causing the title to be off-center when no onActionTap was provided.
Now padding is only applied when the corresponding action button is
actually rendered.

* test(WnSlate): add test for flightShuttleBuilder to achieve 100% coverage

Directly invoke the flightShuttleBuilder callback to verify it returns
the expected Material > Container structure with correct properties.

* fix: use backgroundSecondary for scroll effects and .w scaling in test

- Change WnScrollEdgeEffect color from backgroundPrimary to backgroundSecondary
- Use flutter_screenutil .w scaling for EdgeInsets in flightShuttleBuilder test

* feat(widgets): add WnSlateAvatarHeader basic structure

* test(widgets): add comprehensive tests for WnSlateAvatarHeader

* feat(widgets): add WnSlateNavigationHeader widget

Navigation header component for slates with back/close actions.
Supports two navigation types: close (right side) and back (left side).
Includes comprehensive test coverage.

* refactor(widgets): replace WnSlateHeader with WnSlateAvatarHeader and WnSlateNavigationHeader

- Delete old WnSlateHeader widget and its tests
- Remove newChat from WnSlateHeaderActionType enum (too specific)
- Update WnSlateHeaderAction to only support close and back types
- Update wn_slate_test.dart to use WnSlateNavigationHeader

* refactor(widgets): inline WnSlateHeaderAction into WnSlateNavigationHeader

* fix(widgets): update slate colors to match Figma design

- Change WnSlate background from backgroundPrimary to backgroundSecondary
- Add borderTertiary border to WnSlate container
- Change WnSlateNavigationHeader icon color to backgroundContentSecondary

* feat(widgets): add WnSystemNotice widget with WnSlate integration

- Add WnSystemNotice widget with types (neutral, info, success, warning, error)
- Add variants (temporary, dismissible, collapsed, expanded)
- Support for title, description, and action buttons
- Integrate systemNotice slot into WnSlate
- Add comprehensive tests for both widgets

* refactor(widgets): clean up WnSystemNotice and tests

- Remove redundant fontFamily: 'Manrope' from TextStyle declarations to
  inherit global theme font
- Remove unused imports (flutter/material.dart, sloth/theme.dart) from tests
- Remove redundant default enum parameters in test assertions
- Make WnSystemNotice const in 'renders actions when present' test

* refactor: add keys to WnSystemNotice icons and move FinderExtensions to test_helpers

- Add Key('systemNotice_leadingIcon') to leading icon for test targeting
- Add Key('systemNotice_actionIcon') to action icon for test targeting
- Move bySvgPath FinderExtensions from wn_system_notice_test.dart to
  test_helpers.dart for reuse across tests

* refactor(widgets): improve WnSystemNotice code quality and tests

- Extract long visibility condition into shouldShowDetails local variable
- Only render action icon when corresponding callback exists
- Replace find.bySvgPath with find.byKey in tests per guidelines
- Remove FinderExtensions.bySvgPath from test_helpers
- Fix line width violations in test declarations
- Add tests for action icon callback requirement

* refactor(widgets): convert WnSystemNotice to HookWidget with animations

Add slide animations and auto-hide functionality to the system notice
widget. The widget now animates in/out and can automatically dismiss
after a configurable duration.

* docs: add slate content transitions design plan

* feat(widgets): add content transitions to WnSlate

Integrate blur/fade content transitions into WnSlate component:

- Add WnSlateContentTransition widget that self-animates after route
  completes (250ms duration, easeInOutCubicEmphasized curve)
- WnSlate now wraps content with transition, driven by route animation
- Add animateContent parameter to WnSlate for opt-out capability
- Simplify routes.dart by removing page-level blur/fade transition
- Refactor screens to use unified WnSlate component
- Remove deprecated WnSlateContainer and WnScreenHeader widgets

The content transition waits for Hero animations to complete before
fading in, ensuring clean sequential animations.

* feat(slate): add dynamic scroll edge effects

- Convert WnSlate to HookWidget to track scroll state
- Show top scroll effect only when scrolled down (content above)
- Show bottom scroll effect only when content extends below
- Add footer parameter for fixed content below scroll area
- Reduce slate scroll effect height from 80.h to 32.h
- Enable scroll effects on settings, edit profile, and network screens
- Move edit profile action buttons to footer for proper effect placement

* fix(tests): update navigation button keys for back navigation

Update tests to use slate_back_button instead of slate_close_button
to match the WnSlateNavigationType.back added to edit profile and
network screens.

* chore: remove plan doc and ignore docs/plans directory

* fix: use correct screenutil units for vertical padding

- Change bottom padding from .w to .h in EdgeInsets.fromLTRB across all
  screens (add_profile, app_settings, chat_info, donate, edit_profile,
  error, login, network, profile_keys, settings, share_profile, sign_out,
  signup, start_chat, switch_profile, user_search, wip)
- Use .r for BoxShadow blurRadius and spreadRadius in wn_slate.dart
- Add TODO comment for skipped keyboard scroll test

* fix(edit-profile): use medium button size and 'Discard' text for footer actions

- Add 'discard' localization key to all language files
- Update button text from 'Discard changes' to 'Discard'
- Use medium button size (44px)

* fix: address PR #128 code review feedback

- Add horizontal padding to WnChatHeader for better layout
- Reduce WnSlateNavigationHeader padding to prevent title truncation
- Remove bottom padding in user_search_screen and fix WnFadeOverlay color
- Add error handling for follow/unfollow in chat_info_screen
- Use localized string for add profile screen title
- Fix responsive sizing (.r for blurRadius, .h for bottom padding)
- Update test expectations and remove unnecessary comments

* fix: improve UI consistency and add developer settings notifications

- Change all slate buttons from large to medium size across screens
- Add system notifications in developer settings for action confirmations
- Fix sign out screen double horizontal padding issue
- Fix home screen RenderFlex overflow with SingleChildScrollView
- Use localized strings in WnAuthButtonsContainer
- Add key package success message localizations for all 8 locales

* test(screens): add tests to improve coverage from 98.7% to 99.3%

Add tests for system notice display, error handling, and navigation:
- chat_info_screen: copy notice, error notice, navigation
- developer_settings_screen: action button success notices
- donate_screen: auto-dismiss timeout
- start_chat_screen: copy notice

* fix: UI improvements and localization updates

- Shorten loginTitle to just 'Login' (and translations) to prevent truncation
- Add vertical padding to WnSlate in chat screen for proper header spacing
- Remove WnAccountBar from onboarding screen (single slate per screen)
- Wire onboarding buttons to correct screens (share profile, user search)
- Add key to login button for unambiguous test targeting

* test(widgets): add coverage tests for WnScrollEdgeEffect constructors

Add tests for canvasTop, canvasBottom, dropdownTop, and dropdownBottom
constructors to achieve 100% coverage on wn_scroll_edge_effect.dart.

* fix: improve concurrency guards, error handling, and responsive sizing

- Add isLoading guard to fetch, publish, deleteAll in useKeyPackages hook
- Set chat info screen navigation header to back type
- Add try/catch and failure handling to developer settings handleAction
- Use height-based units for vertical padding in home and onboarding screens
- Update test to match back navigation button key

* Additional tests and update the android binaries

* test(signup): add keyboard scroll assertions to signup screen test

Replace direct physicalSize manipulation with proper viewport setup,
simulate keyboard via viewInsets, and add assertions to verify the
Sign Up button remains visible when keyboard appears.
2026-02-02 14:00:30 +01:00
JeffGandGitHub f45c1ff6a9 feat(widgetbook): add component use cases and fix asset loading (#135)
* feat(widgetbook): add component use cases and fix asset loading

Add widgetbook use cases for buttons, feedback, icons, inputs, menu,
and structure components. Fix SVG asset loading by adding package
parameter to SvgPicture.asset calls so assets resolve correctly when
running in the widgetbook app.

* feat(widgetbook): add comprehensive use cases for all components

Add detailed use cases with knobs for buttons, feedback, icons, inputs,
menu, and structure components. Enables interactive testing of all
widget variants and states.

* refactor(widgetbook): use symlink for assets to avoid package prefix

Add symlink from widgetbook/assets to parent assets directory. This
allows widgets to load SVGs without needing package: 'sloth' prefix,
simplifying asset loading across both main app and widgetbook.

* refactor(widgetbook): clean up structure.dart and fix scroll edge heights

- Remove decorative banner comment blocks to follow project style
- Add height parameter to _StaticScrollEdgeEffect widget
- Pass correct heights: canvas=48px, slate=80px, dropdown=40px

* feat(widgetbook): constrain playground widgets to phone width

Wrap interactive playground widgets in Align + ConstrainedBox to limit
their width to 375px (iPhone width) instead of expanding to full width.

* refactor(widgetbook): remove ViewportAddon from addons

Device viewport simulation handled differently now.

* fix(widgetbook): convert switch statements to switch expressions for exhaustiveness

Convert two switch statements in structure.dart to switch expressions
to satisfy Dart's switch exhaustiveness rules and eliminate compile errors.
2026-02-01 09:18:03 +01:00
JeffGandGitHub 3abaa684c6 feat(theme,widgets): add overlay colors and WnOverlay component (#129)
* feat(theme,widgets): add overlay colors and WnOverlay component

Add overlayPrimary semantic color (whiteAlpha500 light, blackAlpha500 dark)
and WnOverlay widget with backdrop blur for modal backgrounds.

Closes #106, closes #107

* test(widgets): improve WnOverlay test names and add dark theme coverage

- Rename misleading 'uses default sigma values' to 'creates a blur filter'
- Add dark theme test to verify blackAlpha500 is applied correctly
2026-01-30 08:59:44 +01:00
JeffGandGitHub a1530b708d feat(widgets): add input components (#115)
* feat(widgets): add slate design input components

Replace WnTextFormField with new slate design input widgets:

- WnInput: Single-line text input with optional label, helper text,
  inline action, and trailing action support
- WnInputPassword: Password input with visibility toggle, scan QR
  button, and paste/clear actions
- WnInputTextArea: Multi-line text input with configurable minimum
  height based on size variant

All components support two sizes (44px and 56px) matching the Figma
design system, with consistent typography (Manrope Medium 14px) and
semantic colors.

Closes #65

* fix: add .toInt() to clamp result and improve password input test coverage

- Fix type error in wn_copyable_field.dart (clamp returns num, need int)
- Add tests for labelHelpIcon callback in WnInputPassword
- All input widgets now have 100% test coverage

* fix: improve input widgets and test reliability

- Add obscurable to useEffect deps in WnCopyableField
- Use .h for height sizing in WnCopyableField Container
- Add key to password field container for testability
- Add second toggle check in visibility test
- Update size tests to assert actual rendered heights
- Fix mountWidget/mountStackedWidget to call setUpTestView

* fix: improve WnInputPassword responsive sizing and disabled state

- Use backgroundSecondary for disabled background (visual distinction)
- Split inlineActionSize into separate width (.w) and height (.h) vars
- Apply same pattern to _buildInlineAction and _buildTrailingAction
- Fix labelHelpIcon SizedBox to use .h for height

* fix(widgets): align input background color with Figma design

Remove redundant ternary for background color in WnInput and
WnInputPassword. Per Figma design, both enabled and disabled states
use backgroundPrimary - disabled state is distinguished by text color.

* refactor(widgets): clean up WnInput code

- Remove redundant Padding with EdgeInsets.all(0.w) around help icon
- Use spread operator to combine inlineAction SizedBox and Gap into
  single conditional block

* refactor(widgets): improve WnInput tap target and deduplicate styles

- Add HitTestBehavior.opaque to label help icon GestureDetector so
  full 18.w SizedBox is tappable, not just the 14.w icon
- Extract _baseInfoTextStyle getter to deduplicate TextStyle in
  _buildHelperText and _buildErrorText methods

* feat(widgets): add focus/hover states and disable interactions for WnInput

- Convert WnInput to HookWidget to track focus and hover state
- Add _getBorderColor method that computes border based on enabled,
  focused, hovered, and error states
- Add MouseRegion for hover detection and Focus wrapper for focus tracking
- Wrap inlineAction and trailingAction with IgnorePointer when disabled
  to prevent interactions on disabled inputs

* fix(widgets): always register MouseRegion callbacks in WnInput

Keep onEnter and onExit callbacks always registered but change behavior:
- onEnter only sets hover state when enabled
- onExit always clears hover state to prevent stale state on disabled widgets

* fix(widgets): use backgroundSecondary for disabled WnInputTextArea

* fix(widgets): use .h for height values in WnInput components

Per AGENTS.md guidelines, use .w for width-based values and .h for
height-based values with flutter_screenutil.

* test(widgets): add hover state tests for WnInput to achieve 100% coverage

* test(widgets): improve WnInput tests to verify actual visual behavior

- Add 'input_field_container' key to WnInput for testability
- Update size tests to verify actual rendered height (56px vs 44px)
- Update hover tests to verify border color changes:
  - tertiary -> secondary on hover
  - secondary -> tertiary on exit
  - tertiary stays tertiary when disabled and hovered

* Update formatting

* refactor(widgets): improve input widget consistency and accessibility

- Extract shared _baseInfoTextStyle in WnInputPassword for helper/error text
- Add HitTestBehavior.opaque to labelHelpIcon GestureDetector for full tap area
- Gate inline action buttons by enabled state in WnInputPassword
- Fix height unit from .w to .h in WnInputTextArea help icon SizedBox
- Add height assertions to WnInputTextArea size tests

* fix(widgets): use correct semantic color for error text

Change error text color from fillDestructive to backgroundContentDestructive
to match Figma design specifications.

Fixes PR #115 review comments.
2026-01-30 08:30:16 +01:00
JeffGandGitHub 487041d5d8 fix(api): serialize tags as structured arrays instead of Debug format (#130)
Replace Debug trait formatting with proper structured serialization for
event tags in FlutterEvent and ChatMessage types. Tags are now serialized
as Vec<Vec<String>> using Tag::as_slice(), which is faster, stable across
Rust versions, and easier to parse on the Flutter side.

Closes #123
2026-01-29 17:30:52 +01:00
JeffGandGitHub f363483d88 feat(widgets): add WnCallout component replacing WnWarningBox (#113)
* feat(widgets): add WnCallout component replacing WnWarningBox

Implement new Callout component based on Figma design with support for
multiple types (neutral, info, warning, success, error) and dismissible
variant. Migrate existing WnWarningBox usages to the new component.

- Add WnCallout widget with CalloutType enum
- Add comprehensive tests for all callout types
- Migrate sign_out_screen, profile_keys_screen, edit_profile_screen
- Remove deprecated WnWarningBox widget and tests

Closes #71

* refactor(tests): consolidate redundant tests and fix mutable state

- Remove redundant 'displays help icon for neutral type' test (covered by
  'defaults to neutral type' test)
- Use mutable container for dismissed state in onDismiss test

* fix(widgets): use responsive .sp values for letterSpacing in WnCallout

* fix(widgets): add left border to WnCallout matching Figma design

- Add borderColor field to _CalloutColorScheme
- Add 4px left border to callout container using type-specific colors
- Fix edit_profile_screen to use neutral type (default) instead of info
- Add test to verify left border is present

* fix(callout): remove left border and use warning type for key screens

- Remove left border from WnCallout to match designs
- Change callout type from error to warning on profile keys and sign out screens
- Remove unused borderColor from _CalloutColorScheme
- Update tests to reflect border removal

* refactor(callout): add stable key to icon for cleaner test assertions

- Add Key('callout_icon') to WnIcon in WnCallout widget
- Update icon tests to use find.byKey instead of iterating find.byType

* fix(callout): add 1px border to match design

Add Border.all with iconColor and fixed 1px width to BoxDecoration.

* fix(callout): use consistent borderTertiary color for border

Border should always use colors.borderTertiary per Figma design,
not change color based on callout type.
2026-01-29 14:23:34 +01:00
JeffGandGitHub 908522864c feat: add WnScrollEdgeEffect widget for scroll edge fade effects (#116)
Implements issue #70 - Scroll edge effect component with support for:
- Three types: Canvas, Slate, and Dropdown
- Top and bottom positioning
- Gradient fade overlay that doesn't block user input
- Configurable height with sensible defaults per type

Closes #70
2026-01-29 11:13:26 +01:00
JeffGandGitHub 546286eecb fix(dropdown): update WnDropdownSelector to match Figma design (#100)
* fix(dropdown): update WnDropdownSelector to match Figma design

- Update border colors: default borderTertiary, pressed borderSecondary
- Add press/tap state for mobile interaction
- Fix font to Medium (w500) with 0.4 letter spacing
- Adjust padding: 4px outer + 8px text padding
- Reduce icon size from 24px to 16px
- Fix text colors: placeholder/unselected use secondary color
- Use backgroundTertiary for selected item background
- Align checkmark icon with close icon using 36px wrapper
- Remove hairline border that caused visual artifact on overscroll

Closes #72

* chore: remove comments and use .sp for letter spacing

* test(dropdown): add tests for press states and animation

* refactor(widgets): remove redundant styles in WnDropdownSelector

- Remove fontFamily from TextStyles (inherited from theme)
- Simplify borderRadius conditional (both branches were identical)

* refactor(widgets): use explicit null-safe label variable in dropdown

Extract displayLabel with null-coalescing for cleaner null handling.

* fix(widgets): align checkmark icon with header icon in large dropdowns

Pass size through to _DropdownItem so checkmark wrapper uses 48.w for
large dropdowns (matching header) and 36.w for small.
2026-01-29 11:02:56 +01:00
JeffGandGitHub 663e7b2f65 feat: add WnSeparator component (#111)
* feat: add WnSeparator component with horizontal/vertical orientation

Implements the Separator design component from Figma (issue #68).

- Add WnSeparator widget with configurable orientation, thickness, indent, and color
- Default to borderTertiary color from theme
- Include comprehensive test coverage (33 tests)

Closes #68

* refactor: use ScreenUtil for responsive sizing in WnSeparator

Apply flutter_screenutil scaling to thickness (.r) and indents (.w)
to follow the repo's responsive sizing convention.

Update tests to verify behavior rather than exact pixel values.

* refactor: remove thickness property from WnSeparator

Thickness is always 1px per design spec, no need for customization.

* fix(separator): use .h for vertical margins in WnSeparator

Use correct screenutil extension for vertical margins (top/bottom)
in _buildVertical method. The .h extension is for height/vertical
dimensions per codebase convention.
2026-01-29 10:31:51 +01:00
JeffGandGitHub d47296b4c6 chore: update WnMenuItem hover states and refresh native libs (#98)
Replace Material InkWell with MouseRegion and GestureDetector for
explicit hover/press handling. Content color now dims on interaction
for all menu item types. Includes updated .so binaries.
2026-01-29 08:33:19 +01:00
JeffGandGitHub 99778fcdcb feat: add WnMenu and WnMenuItem components with quiet precommit (#92)
* feat: add WnMenu and WnMenuItem components with quiet precommit

- Add WnMenu component for displaying lists of menu items
- Add WnMenuItem with primary/secondary/destructive types and optional icons
- Refactor settings screen to use WnMenu/WnMenuItem instead of _SettingsTile
- Add quiet variants for test commands (test-flutter-quiet, test-rust-quiet)
- Update precommit to show minimal output with step names + pass/fail status
- Add precommit-verbose for debugging failures
- Update AGENTS.md with documentation for quiet commands and commit checklist

* fix: address code quality issues in justfile, docs, and widget

- justfile: use mktemp for unique temp files in _run-quiet recipe
  to prevent collisions and add trap for cleanup on EXIT
- AGENTS.md: add language tag and blank line to code fence for
  markdownlint compliance
- wn_menu_item: add maxLines: 1 to Text widget for reliable ellipsis

* refactor(widgets): address PR review feedback

- Replace Container with SizedBox in WnMenuItem (remove unnecessary padding)
- Use Column spacing parameter in WnMenu instead of manual loop
- Add .sp suffix to letterSpacing for device scaling
2026-01-27 20:29:24 +01:00
JeffGandGitHub f5144ffa91 feat(settings): redesign header with Share & connect button (#94)
* feat(settings): redesign header with Share & connect button

- Replace inline QR icon button with full-width 'Share & connect' primary button
- Add 'Switch profile' button below with proper 8px spacing between buttons
- Update text styles to match Figma design (line height, letter spacing)
- Add shareAndConnect and switchProfile localization keys for all 8 languages
- Update tests to reflect new UI structure

* fix(settings): address PR review feedback

- Use .sp suffix for letterSpacing values (responsive scaling)
- Add change icon to Switch Profile button
2026-01-27 20:29:09 +01:00
JeffGandGitHub b9424ed299 Consolidate WnFilledButton and WnOutlinedButton into unified WnButton (#90)
* Consolidate WnFilledButton and WnOutlinedButton into unified WnButton

Replace two separate button widgets with a single WnButton component that supports
multiple button types (primary, outline, ghost, overlay, destructive) and sizes
(large, medium, small) via enums. This simplifies the button API and reduces code
duplication across the codebase.

* Fix button text overflow with Flexible wrapper and ellipsis

* Fix WnButton crash in unbounded-width parents with LayoutBuilder

* Remove unused height property from WnButtonSize enum

- Simplify WnButtonSize to plain enum without height values that were
  never used (button sizing is handled via _getVerticalPadding())
- Remove tests for the deleted height property
- Remove redundant test in wn_auth_buttons_container_test.dart
2026-01-27 14:48:22 +01:00
923fadbc4d Add internationalization support with 8 languages (#83)
* Add internationalization support with 8 languages

- Add locale provider with system/manual language selection
- Add ARB translation files for EN, ES, FR, DE, IT, PT, RU, TR
- Localize all 19 screens and 23 widgets
- Add locale-aware date/number/currency formatters
- Add comprehensive locale provider tests (40+ tests)
- Add language selector in app settings
- Include proper ICU plural forms for all languages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Formatting

* Regenerate flutter_rust_bridge bindings after rebase

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Formatting

* Improve locale persistence and add profile error strings

- Refactor LocaleNotifier to use secure storage for locale preference
- Add profileLoadError and profileSaveError localization strings
- Update tests for new locale provider implementation
- Fix various test issues and remove unused imports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Improve test coverage and fix CI l10n validation

- Fix CI workflow to use jq for reliable ARB key extraction instead of grep
- Add error handling tests for LocaleProvider storage failures
- Add failure simulation flags to MockWnApi and MockSecureStorage
- Add tests for LocaleFormatters, LocalePersistenceException, and edge cases
- Add UI error handling tests for language update and npub conversion failures

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Improve dropdown scroll UX and fix test reliability

- Add scroll fade indicators to dropdown when options exceed visible items
- Refactor dropdown list into _ScrollableDropdownList widget
- Improve CI l10n validation with better jq error handling
- Remove unused _resolveSystemLanguageCode method
- Fix async test expectations in locale provider tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add atomic locale storage rollback and CI orphaned key detection

- Locale provider now captures previous preference before changes and
  restores it atomically if the rust API call fails
- CI l10n validation now detects extra/orphaned keys in locale files
  (not just missing keys), with improved error messaging
- Added tests for storage rollback behavior on API failure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Remove unused import

* refactor: move l10n keys validation to scrip folder

* Update spanish localizations

* fix(l10n): improve Spanish key terminology (clave → llave)

Use 'llave' instead of 'clave' for cryptographic keys in Spanish
translations, which is more commonly used in technical contexts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* ci: add pub cache to l10n-validation and locale validation to precommit

- Add Flutter pub cache step to l10n-validation CI job for faster runs
- Add validate-locales-keys to precommit and precommit-check recipes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ci): use correct github.workspace context in l10n cache

Replace runner.workspace with github.workspace for the pub-cache
path in l10n-validation job. runner.workspace is not a valid
GitHub Actions context.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: josefinalliende <mariajosefinaalliende@gmail.com>
2026-01-27 09:38:01 +01:00
c4e31d5330 Add more and add_large icons, replace remaining Material icon in chat header (#87)
- Add add_large.svg and more.svg icons
- Update clean, help_filled, paste, zap SVG icons
- Register new icons in WnIcons enum
- Replace Material Icons.more_horiz with WnIcon.more in chat header

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 14:57:54 +01:00
157ae4e429 Update semantic colors with intention system and quaternary colors (#79)
- Add new intention colors (info, success, warning, error) with background/content variants
- Replace borderInfo/borderSuccess/borderWarning with intention color system
- Add backgroundContentQuaternary and fillQuaternary color properties
- Add alpha transparency primitives (BlackAlpha, WhiteAlpha)
- Swap destructive color hierarchy (red600 primary, red500 secondary)
- Add missing primitive colors (red50/950, green50/600/950, blue600, orange600)
- Update wn_relay_tile to use intentionSuccessContent

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 15:19:35 +01:00
3a91466d2a Replace Material icons with custom SVG icon system (#80)
* Replace Material icons with custom SVG icon system

- Add WnIcon widget with WnIcons enum for type-safe icon usage
- Replace all Icons.xxx with WnIcon(WnIcons.xxx, ...) across codebase
- Add 70+ new SVG icons to assets/svgs/
- Update WnWarningBox to accept WnIcons instead of IconData
- Refactor _SettingsTile to use WnIcons for consistency
- Update test files to use WnIcon-based finders

Closes #14

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add tests for WnIcon widget

- Test WnIcons enum path generation
- Test unique filenames across all icons
- Test widget rendering, sizing, and color filtering

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix icon improvements: Center send button, IconTheme fallback, stable test finders

- Wrap send button WnIcon in Center widget for proper positioning
- Add IconTheme.of(context).color fallback for theme-aware icon coloring
- Update test files to use key-based finders instead of fragile type finders
- Add tests for IconTheme behavior in WnIcon widget

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix copy icon disabled state visual affordance

Apply disabled-aware color to copy icon so it dims when npub is null,
matching standard IconButton behavior.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add comprehensive tests for icons and themes

- WnIcon: size edge cases, color variations, nested IconTheme inheritance
- Theme: SemanticColors copyWith/lerp, AccentColorSet operations
- SemanticAccentColors: all 12 colors, lerp transitions
- BuildContext extension: light/dark themes, fallback behavior
- Profile keys test: scope visibility icon finder with find.descendant
- wn_icon test: rename misleading test name

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix search icon padding and add tests

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 15:18:18 +01:00
5eb6e98999 Redesign WnDropdownSelector with expanding animation (#75)
* Redesign WnDropdownSelector with expanding animation

Replace Flutter's DropdownButton with custom expanding dropdown:
- Add size variants (small: 44px, large: 56px)
- Implement 120ms expanding animation instead of overlay
- Add checkmark for selected/hovered items
- Add helper text and error/disabled states
- Toggle chevron to X icon when open
- Fix icon alignment with consistent 24sp sizing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Formatting

* Fix WnDropdownSelector overflow and disabled hover state

- Add max visible items (5) to prevent unbounded height growth
- Replace SingleChildScrollView with ListView.builder for scrollable options
- Use BouncingScrollPhysics instead of NeverScrollableScrollPhysics
- Add disabled guards to MouseRegion handlers to prevent hover state changes
- Update borderColor logic to check isDisabled before hover/open styling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add comprehensive tests for WnDropdownSelector

- Test closing dropdown by tapping header again
- Test error state border and helper text styling
- Test hover state on enabled dropdown (MouseRegion)
- Test hover ignored when dropdown is disabled
- Test scrollable list with more than 5 options
- Test checkmark appears on hover over options
- Test graceful handling of value not in options

Restores coverage from 99.36% to 99.57% (dropdown now at 100%)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Handle disabled state changes while dropdown is open

- Add didUpdateWidget to close dropdown when isDisabled flips to true
- Add early-return in _selectOption when disabled
- Pass isDisabled to _DropdownItem for disabled hover/tap handling
- _DropdownItem now ignores hover and tap events when disabled
- Add tests for disabled state transitions and blocked interactions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Refactor WnDropdownSelector to HookWidget, remove hover state

- Convert WnDropdownSelector from StatefulWidget to HookWidget
- Use useState, useAnimationController, useMemoized, useEffect hooks
- Convert _DropdownItem from StatefulWidget to StatelessWidget
- Remove all hover state (MouseRegion, _isHovered) for mobile-first
- Remove hover-related tests (3 tests removed, 27 remain)
- Update AGENTS.md with StatefulWidget avoidance guideline

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Use keys to find icons in tests, rename size variant tests

- Add key to checkmark icon in _DropdownItem
- Replace find.byIcon() with find.byKey() in all tests
- Rename "respects" to "supports" in size variant test descriptions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add inline comment explaining +2 border compensation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix test to tap header text instead of icon

Update 'closes dropdown when tapping header again' test to actually
tap the header text, matching the test description.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Remove all comments

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 15:27:54 +01:00
2cbdc1de11 Add semantic color system with ThemeExtension (#55)
* Add semantic color system with ThemeExtension

Implement a comprehensive semantic color system based on designer specs:

- Add SemanticColors ThemeExtension with light/dark theme support
- Include all semantic tokens: backgrounds, fills, borders, accents
- Add 12 accent color sets (blue, cyan, emerald, etc.)
- Implement proper copyWith and lerp for theme transitions
- Add context.colors extension for easy access

Migrate all existing widgets and screens to use semantic color naming:
- foregroundPrimary -> backgroundContentPrimary
- foregroundSecondary -> fillContentPrimary
- foregroundTertiary -> backgroundContentTertiary
- error -> fillDestructive
- success -> borderSuccess

Remove old ThemeColors type and build_context.dart extension.

* Fix semantic color issues and add comprehensive tests

- Fix indigo dark accent border to use indigo200 instead of blue200
- Fix image picker initials contrast by using backgroundContentPrimary
- Add comprehensive tests for SemanticColors, AccentColorSet, and extension

* Fix deprecated Color.value usage in semantic colors tests

* Fix/semantic colors (#56)

* fix: replace bg secondary with fill primary

* fix: replace color borders with border theme colors

* feat: leave shadow black for both themes

* fix: replace opacity with colors and change colors for texts difficult to read

* Add theme tests for font family and SemanticColors extension

Expand theme_test.dart from 2 to 6 tests covering:
- Manrope font family configuration for light/dark themes
- SemanticColors extension attachment for light/dark themes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add tests for WnAnimatedPixelOverlay widget

Add 10 widget tests covering:
- Basic rendering at various progress values (0, 0.5, 1.0)
- Custom pixelSize, shouldDrawThreshold, and seed parameters
- Correct dimension handling (width/height)
- Default value verification
- Color and progress property storage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add test for nsec remaining null on error

Tests the negative case where copyWith preserves null nsec value
when an error occurs during initial load.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Pepi <mariajosefinaalliende@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 21:45:16 +01:00
JeffGandGitHub 41dfbaadb2 Add app settings screen with theme selector (#52)
* Add app settings screen with theme selector

- Create AppSettingsScreen with theme dropdown (System/Light/Dark)
- Add ThemeNotifier provider for reactive theme state management
- Add WnDropdownSelector reusable widget for dropdown selections
- Add Rust helper functions for ThemeMode conversion
- Update settings screen to navigate to AppSettingsScreen
- Theme preference persists via Rust API with graceful error handling

Closes #25

* Address PR comments: refactor AppSettingsScreen and mocks

* Fix dropdown to be controlled and improve test helpers

- Switch WnDropdownSelector from DropdownButtonFormField with initialValue
  to controlled DropdownButton with value prop so it reflects provider updates
- Update pumpAppSettingsScreen to accept optional initialThemeMode parameter
  instead of always overwriting mockApi.currentThemeMode
- Replace find.byIcon() with find.byKey() in tests for dropdown icon
- Add testing guidance to AGENTS.md about preferring find.byKey() over find.byIcon()
2026-01-20 08:04:38 +01:00
JeffGandGitHub f1790ae4ba Fix login exception (#45)
* Add sign out screen with private key backup and warning

- Create dedicated SignOutScreen with warning about chat deletion
- Allow users to backup private key before signing out
- Fix logout exception by handling null pubkey state in SettingsScreen
- Update settings to navigate to SignOutScreen instead of inline logout

* Extract WnSecretField widget for key display with copy/visibility

- Create reusable WnSecretField widget for displaying keys with copy button
  and optional visibility toggle
- Refactor ProfileKeysScreen to use WnSecretField for public and private keys
- Refactor SignOutScreen to use WnSecretField for private key backup
- Add comprehensive tests for WnSecretField widget

* Rename WnSecretField to WnCopyableField for clarity

The widget is a read-only copyable text field with optional obscuring,
not specifically for secrets. This rename better reflects its purpose.

Also simplified the widget by having it manage its own TextEditingController
internally, removing the need for callers to pass one.

* Fix hook ordering and clipboard behavior in settings screens

- Move useUserMetadata call before early return in SettingsScreen to maintain
  consistent hook invocation order across builds
- Update useUserMetadata to accept nullable pubkey and return safe default
- Fix WnCopyableField to copy controller.text instead of stale value prop
- Sort import statements alphabetically in profile_keys_screen and sign_out_screen
- Update AGENTS.md commit checklist wording

* Fix hook ordering in SignOutScreen and add comprehensive tests

- Move hooks before early return in SignOutScreen to maintain consistent
  hook invocation order across builds
- Update useNsec to accept nullable pubkey and make loadNsec a no-op when
  pubkey is null
- Add tests for useUserMetadata with null pubkey
- Add tests for useNsec with null pubkey
- Add test for WnCopyableField to verify copy uses controller.text
- Add test for SettingsScreen null pubkey handling
2026-01-19 14:34:40 +01:00
JeffGandGitHub ffc3bcca28 Add OpenCode context: agents configuration, skills, and git worktrees (#44)
* Add OpenCode context: agents configuration, skills, and git worktrees

* Update .gitignore to include Git worktrees and remove .opencode dir

* Update AGENTS.md: add language identifiers to code blocks and test coverage note
2026-01-18 13:00:05 +01:00