On a cold start the splash was followed by about a second of blank
white, then mail rows whose header sat under the status bar until it
popped in and pushed the list down.
- `AppTheme` had no `android:windowBackground`, so while JS loaded the
window showed the platform default (near-white, or grey at night).
It is now the app's own background, #ffffff by day and #09090b at
night (`values-night`), matching `src/theme/tokens.ts`.
- `NavigationContainer` had no theme, so React Navigation painted its
light default behind and between screens even in dark mode. It now
uses the palette's background.
- The mail list took its top inset from the native `SafeAreaView`,
which on Fabric only learns the insets a few frames after mounting.
It now pads with `useSafeAreaInsets()`, which the navigator's
safe-area provider fills from the initial window metrics on the first
render; the value is the same, it just arrives in time.
Reminders were rebuilt from the events of the window the calendar
showed, and everything else was cancelled, so looking at another month
dropped every pending reminder. The sync only started once the Calendar
tab opened, and without a notification handler a reminder that fired
while the app was open was never shown.
- load the events for reminders from their own window (now to a week
past the horizon), independent of the view, via `loadEventsInRange`
factored out of `fetchEvents`
- leave scheduled reminders alone when that window can't be loaded
(offline, no session yet), cancel only ours, and cancel them all on
sign-out
- start the sync at launch and rerun it after sign-in or an account
switch, on `Calendar`/`CalendarEvent` state changes, on in-app edits
and when the app returns to the foreground; only the Calendar tab may
ask for notification permission
- install a foreground handler that shows our reminders; mail push is
posted natively and doesn't go through expo-notifications
The notification only carried the local account it was posted for, so
tapping one for a message in a group or shared mailbox opened the
thread against the user's own JMAP account, where the message doesn't
exist.
The background task now passes the JMAP account the message was
fetched from as `jmapAccountId`. The native module stores it as an
intent extra, validated like the other ids, and hands it back with the
tap. `App.tsx` routes the open with it, the way the unified inbox does
for group messages. Notifications posted before this change have no
such extra and open against the account's own mail as before.
The launch-time effect in `App.tsx` re-ran the full setup for the
active account whenever a relay URL was stored, and that URL is shared
by every account on the device. Turning push off for one account, or
revoking this device, lasted only until the next launch or token
rotation. A subscription revoked from another device was re-created
without asking.
- Turning push off in settings and revoking this device now go through
`disablePushForAccount`, which tears the registration down and marks
the account as opted out. Enabling push again clears the mark.
- The effect calls `resyncPushNotifications`, which skips opted-out
accounts. If the server no longer has the recorded subscription
although it still had a day or more to live, it treats the
subscription as revoked and turns push off for the account instead
of registering again. A subscription that lapsed is re-created as
before.
- Setup records the expiry the server reports: from the create
response (`createPushSubscription` now returns it) and from every
listing.
Stalwart ids are only unique per account. When its opener named no
account, the viewer used the account of the folder open in the list,
and it resolved Trash, Junk and move targets from that folder as well.
Deleting a team message from the Unified Inbox therefore sent
`Email/set` to the user's own account and trashed an unrelated message
with the same id, and a message opened from a notification, deep link
or contact while a shared folder was open went to the shared account.
- the list names the open folder's account when it opens the viewer,
and the viewer takes the account from its opener only, fixed at open
- Trash, Junk, Inbox and move targets come from the message's account,
and delete and move take the message out of its own folder
- the store's delete and move take the viewed message: they only touch
the list's row when the list shows that account, and refuse folders
of any other account
- another account's message is marked read on the server directly
instead of through the open folder's list
- quick reply, unsubscribe and read receipts file into the message's
account's folders
- undo no longer slots another account's message into the open list
- Archive is only offered for mail of the open folder's account, since
it still resolves the archive folder from there
- Integrated useColors hook for dynamic theming in SettingsScreen.
- Updated tab labels and titles to use localized strings.
- Refactored styles to utilize theme palette for consistent styling.
- Improved locale subscription to re-render labels on language change.
feat: implement useSheetDrag hook for bottom sheet gesture handling
- Added useSheetDrag hook to manage drag-to-dismiss functionality for bottom sheets.
- Configured pan responder for handling drag gestures and dismissing the sheet based on distance and velocity.
refactor: restructure color and token management
- Introduced useColors and useResolvedTheme hooks for better theme management.
- Separated light and dark color palettes for improved maintainability.
- Updated tokens to reflect new theming structure and ensure consistency across components.
- Implemented ContentSendersSettings component for managing trusted senders.
- Added LanguageSettings component to allow users to select their preferred language.
- Introduced minimal i18n support with locale detection and translation functions.
- Created a locale store using Zustand for managing locale state and persistence with AsyncStorage.
- Added a script to sync locale JSON files from the parent webmail repository.
CI:
- .github/workflows/release-android.yml builds APK on release publish
and via workflow_dispatch, attaches it to the release.
- android/app/build.gradle: optional release signingConfig driven by
Gradle properties (ANDROID_KEYSTORE_BASE64 secret etc.); falls back
to the debug keystore when no secrets are set.
- .gitignore: untrack /android so the runner can build directly;
ignore build outputs (build/, .cxx/), Metro/Expo dev logs, scratch.
Track android/ sources including BulwarkFcmPackage.kt and
google-services.json (Firebase Android keys are public per Google's
docs and intended to ship in the APK).
JS sync: push notifications + background task, vacation responder
(api + store), contact activity component, search utilities, settings
polish, calendar/contacts/email store and screen tweaks.
Tooling: scripts/poll-reload.mjs — Windows polling-watcher that hits
Metro /reload on file change (workaround for unreliable fs.watch
without Watchman).