Commit Graph
491 Commits
Author SHA1 Message Date
Linus Rath e30749a50e ci: stop the android release failing on the runner's new sdk tools 2026-09-24 22:31:01 +02:00
Linus Rath 53c0bc0fc2 chore: update version number to 0.1.63 0.1.63 2026-09-24 22:18:42 +02:00
Linus Rath a8e74c8b6d Merge branch 'main' of https://github.com/bulwarkmail/native 2026-09-24 22:18:06 +02:00
Linus Rath 949bb10ac5 docs: record the device run, the r8 and label fixes and the push crash it found 2026-09-24 22:16:46 +02:00
Linus Rath e5659ed5e8 ci: attach the r8 mapping to android releases
Release APKs are obfuscated since R8 was turned on, and nothing kept
the mapping that decodes their stack traces. The workflow now copies
`android/app/build/outputs/mapping/release/mapping.txt` to
`bulwark-mobile-<version>-<commit>-mapping.txt`, uploads it as a
workflow artifact and attaches it to the release next to the APK. The
in-app updater only picks `.apk` assets, so it ignores the new file.

If R8 is on (`android.enableMinifyInReleaseBuilds=true`) and no mapping
was written, the step fails instead of publishing an APK whose crashes
cannot be read. docs/android-release.md explains how to decode a trace
with `retrace`.
2026-09-24 22:14:35 +02:00
Linus Rath a764a07c65 fix: show a white splash in light mode
The splash background was #09090b in both modes, so in light mode a
cold start went from a near-black splash to the white first frame.
It is now #ffffff by day and #09090b at night (`values-night`), as
`splash.backgroundColor` and `splash.dark.backgroundColor` in
app.config.js already ask for, so a later `expo prebuild` writes the
same colours. Checked on the API 35 emulator: light mode stays white
from splash to login screen, dark mode is unchanged.
2026-09-24 22:14:35 +02:00
Linus Rath 5ddc2df64b fix: stop holding a wake lock for 30 seconds after every push
Every background push kept the app awake for the full 30 s timeout of
the BulwarkPushTask headless task, however quickly the task was done.
AppRegistry reports a finished task through the native
HeadlessJsTaskSupport module, and React Native 0.81 registers that
module only for the old bridge (CoreModulesPackage); the bridgeless
runtime's CoreReactPackage leaves it out. JS could never say it was
done, so the service, or the job, and its wake lock waited for the
timeout. BulwarkHeadlessJsTaskSupportModule provides the module under
the same name when the app runs bridgeless.

The push path also leaked a wake lock now and then. startHeadlessTask
called HeadlessJsTaskService.acquireWakeLockNow on the FCM worker
thread while the service took the same static lock on the main thread.
The method is not thread-safe, so both could create a lock, and the one
that was overwritten stayed held. The FCM thread now takes its own
wake lock, timed at 10 s, and the service drops it as soon as it holds
React Native's.

Measured on the API 35 emulator with the R8 release build:

- push handled by the job: 30,000 ms before, 75-155 ms now
- push handled by the service: the service ran 31 s before, it is
  gone within a second now
- `com.facebook.react.i` wake lock still held 5 s after a push: 2 of
  4 pushes before, 0 of 8 now
2026-09-24 22:14:35 +02:00
Linus Rath fef23e4c33 fix: stop a background push from crashing the app on android 12 and later
A push that arrived while the app was in the background started
BulwarkPushTaskService with `startService`. Android 8+ allows that only
while the app is temporarily allowlisted, which a high-priority FCM
message grants. The relay does send FCM data messages at HIGH priority,
but FCM delivers some at normal priority anyway (it downgrades apps
whose high-priority messages rarely end in a notification), and a
UnifiedPush distributor may not raise the app at all. The call then
threw `BackgroundServiceStartNotAllowedException` and the app crashed,
from the FCM thread or, for UnifiedPush, on the main thread.

The service start is kept as the first choice, so a high-priority push
works as before. When Android refuses it, the same headless task now
runs from a JobScheduler job (BulwarkPushJobService): expedited on
Android 12+, which runs at once while the app has expedited-job quota
and as a regular job after that, and a regular job on Android 8-11.
JobScheduler is a platform API, so nothing is added to the build;
WorkManager is not in the dependency tree. A generic "new mail"
notification built natively was the other option, but the relay's
payload cannot tell new mail from other changes and the native side
knows neither the notification settings nor what was already shown.

Checked on the API 35 emulator with the R8 release build, the process
killed and the push delivered past the allowlist:

- FCM data message: before, FATAL EXCEPTION on
  Firebase-Messaging-Intent-Handle; now the job is scheduled 18 ms
  after the refusal and runs the task
- UnifiedPush message (connector 3.3.5, sent the way a distributor
  does): before, FATAL EXCEPTION on main; now the same job path
- allowlisted FCM message: still starts BulwarkPushTaskService
2026-09-24 22:14:35 +02:00
Linus Rath 4a3bce154c feat: translate the new app strings and screen-reader labels into all 26 languages 2026-09-24 22:06:28 +02:00
Linus Rath 13a3e95a0a chore: drop unused app strings and retranslate the ones that became plurals 2026-09-24 21:45:29 +02:00
Linus Rath fd55e16492 fix: translate the last export errors, dates and participant counts
A sweep of src for what the earlier passes missed:
- The message export errors ("Sharing is not available on this
  device", "Download failed: 404") were English; they come from the
  catalog now.
- Event cards and details glued the participant count to a lowercased
  "Participants"; they use the webmail's participant plural.
- Contact activity dates ignored the app language.
- Imported filter rules from an unknown tool showed the parser's
  English "External" instead of the translated label.
- The contact field remove button and the vacation editor's
  bold/italic/underline/list buttons get screen reader labels.
2026-09-24 21:44:47 +02:00
Linus Rath 5e557266d1 fix: count the files selection and delete prompt with plurals
The files screen filled `files.selected_count` and
`files.delete_items_title` with its own `{name}` substitution, a
leftover from before t() took arguments, so they could not be
plurals. Both are ICU plurals now and every message on the screen gets
its values through t(). The upload progress line is one message
instead of "Uploading..." followed by the numbers.
2026-09-24 21:44:47 +02:00
Linus Rath b178ee8d07 fix: translate the tab bar and label the unified inbox and drawer buttons
The bottom tabs showed their route names ("Mail", "Calendar", …) in
every language; they now use the webmail's navigation names, and an
unavailable tab says so with one translated message instead of an
English "(unavailable)" suffix. The first loading screen is
translated as well.

The Unified Inbox selection toolbar, its back and clear-search buttons,
and the drawer's close, expand/collapse and remove-account buttons get
screen reader labels. Its unknown-sender fallback, selection count and
"account(s) could not be loaded" banner are proper translations and
plurals now.

The key harvest and the translation coverage test did not look at
App.tsx, which sits next to src/; they do now.
2026-09-24 21:44:47 +02:00
Linus Rath 1cbf763a36 fix: send calendar reminders in the app language
Reminder notifications are worded when they are scheduled, outside
React, and were always English ("Starts in 15 min", "(No title)",
"Task due"). They now come from the catalog through the locale store,
with ICU plurals for the minutes, hours and days. Each scheduled
reminder records its language, so switching languages rewords the
pending ones, and the Android channel ("Calendar reminders", shown in
the system settings) is named and renamed in the app language.

The reminder labels picked the English form whenever a catalog entry
was an ICU plural, which is all of them; they now pass the count to
t(), and "minutes after" is one plural message instead of a number
glued to a fragment. The import refusal error, the exported alarm
text and the "sharing is not available" error are translated too.
2026-09-24 21:44:46 +02:00
Linus Rath 84fef142b9 fix: translate the calendar sheets and label their buttons
- The custom recurrence editor and its summary were English throughout
  (labels, units, weekday and month names, "on the third Thursday");
  they now use the webmail's recurrence fragments and the app's date
  locale. The interval and occurrence counts are ICU plurals, also in
  the event details, which filled "Every {count} days" with
  `.replace`.
- The iCal subscription sheet is translated with the webmail's
  subscription strings, and its "last updated" time is relative in the
  app language. The feed errors it shows come from the catalog too.
- The calendar share sheet asked for `calendar.share.role_*` keys no
  catalog has; it now uses the webmail's sharing presets and title.
- Close, remove, refresh, unsubscribe, delete and new-task buttons get
  screen reader labels, the event editor's switches are named, color
  swatches read out their color, and task checkboxes and calendar rows
  report whether they are checked.

The calendar i18n test now covers the sheets.
2026-09-24 21:44:46 +02:00
Linus Rath b3a82f1241 fix: translate the week view and label the calendar toolbar
The week view's "all-day" label and its "Untitled" fallback were
hard-coded; they now use the catalog's "All day" and "(No title)" like
the scrolling time grid. The calendar's menu, tasks, view switch, new
event and previous/next buttons get screen reader labels (the view
buttons also say which one is selected), and a calendar i18n test
covers the calendar views.
2026-09-24 21:44:46 +02:00
Linus Rath 13cba64854 fix: translate the viewer's leftovers and label its icon buttons
The message details panel built its received line, attachment summary
and spam score by gluing strings together; each is now one ICU message
(the attachment count is a proper plural). The SPF/DKIM/DMARC results
use the webmail's translated result names, and an invitation's date is
formatted in the app language instead of always in English.

The quick reply box wrote its own "date, name <email>:" quote header;
it now uses the composer's translated "On {date}, {from} wrote:" line
so both kinds of reply look the same.

The back button, the subject and header star buttons, the sender
avatar and the More/Tag sheet close buttons get screen reader labels,
and the screens i18n test covers the viewer files.
2026-09-24 21:44:46 +02:00
Linus Rath e3501f939d fix: offer undo for replies sent from the viewer
A quick reply held by the undo-send delay could not be undone: the
undo bar lived only on the mail list, and the quick reply never
recorded the held send for it.

- The undo bar is now hosted on every stack screen through the
  navigator's `screenLayout`, next to the toasts, and only the focused
  screen's renders, so it shows wherever the send started and never
  twice. The list's own instance is gone; archive, delete, move and
  spam undo use the same bar as before. The composer gets none, since
  its Undo reopens the composer.
- The quick reply records a held send through the same store action as
  the composer, including the account it went out of, so Undo and
  Send now reach a shared account's submission and Undo restores the
  reply into that account's Drafts.
- The quick reply dismisses the keyboard after sending, which would
  otherwise cover the bar.
2026-09-24 21:16:59 +02:00
Linus Rath 5e3cbf2419 fix: shrink the android release with r8
Release builds had R8 and resource shrinking off, so every class of
every dependency went into the APK unminified: the published 0.1.62 is
42.8 MB for arm64, 25.85 MB of it dex (PF9 in the audit).

Turn both on through the properties build.gradle already reads
(`android.enableMinifyInReleaseBuilds` and
`android.enableShrinkResourcesInReleaseBuilds`). The consumer rules of
React Native, Expo, Firebase, ML Kit and the other libraries cover what
they load by reflection. The app's own package is kept whole, which
costs a few KB and keeps its frames readable in an obfuscated stack
trace.

Measured on a local x86_64 release build:

- APK: 44.7 MB -> 38.0 MB (download size 23.8 MB -> 17.2 MB)
- dex: 30.4 MB in 4 files -> 11.3 MB in 2 files
- classes: 27,631 -> 16,724

With `-dontobfuscate` the APK would be 38.8 MB (13.5 MB of dex), so
obfuscation stays on. Library frames in a release stack trace are now
obfuscated; retrace them with that build's
`android/app/build/outputs/mapping/release/mapping.txt`.

Checked on the emulator: cold start in light and dark mode, German and
Russian strings, SENDTO and VIEW mailto and SEND share intents, every
sign-in screen, the QR scanner with ML Kit, and an FCM data message
starting the headless push task.
2026-09-24 21:13:42 +02:00
Linus Rath 9c031c8761 docs: update the fix-pass notes for the labels and strings that landed since 2026-09-24 21:02:09 +02:00
Linus Rath c81dc5179f docs: tick the parity items this pass completed
Tick the parity items the fix pass finished (server-side recurrence,
filters and vacation for shared accounts, in-app file preview, the
trusted-senders toggle, UnifiedPush, sender taps in the viewer), and
note on the already ticked ones what this pass changed, such as the tag
view and search reaching shared accounts or contact photos as avatars.
Items that only moved forward say what is left.

Fix hints and "verified" lines that named removed functions
(`setEmailKeywords`, `getEmailDetail`, `fetchEmailsChunked`,
`buildRecurrenceOverridePatch`) or the old whole-map keyword writes now
point at what replaced them.

PARITY_CHECKLIST.md counts done and open items per area: 379 of 415 are
done, 36 open.
2026-09-24 21:02:09 +02:00
Linus Rath d72aab2519 docs: correct the stale parity checklist entries
Apply the audit's "Stale parity checklist" section to docs/parity:
- ticked items the audit found broken keep their tick and name the
  commit that fixed them; the date-locale item is unticked again, since
  there is still no `dateLocale` setting
- the items that were done but unticked are ticked with where they
  landed
- natural-language event input, print contact and the Sieve editor
  gutter move to the N/A lists
- the text about linking, push types, RTL, `useTypography`, the Sieve
  "Keep" and `FileNode/get ids:null` now describes main
2026-09-24 21:02:09 +02:00
Linus Rath d4ac1e1df4 docs: record what still needs a device check and a decision after the fix pass
The audit gains a "Fix pass, 2026-09-24" section after the summary. It
says how the pass was done and tested, and lists what the fix agents
could not settle from the code alone:
- the device and server checks that matter before a release
- the product calls, starting with the release signing key and secrets
  that have to exist before the next version bump
- the follow-ups the agents left, one line each
2026-09-24 21:02:08 +02:00
Linus Rath 06ce86e31a docs: tick the audit items fixed in this pass
Every audit item a commit on main fixed is ticked with the commit that
fixed it, including the "fix these ten first" list and the stale parity
entries that the fixes resolved. Items fixed only in part keep their box
and say what is left; U8, PF9, B2 and the product calls stay open.
2026-09-24 21:02:08 +02:00
Linus Rath 6fe907b4bc fix: translate the Themes and Updates settings tab names 2026-09-24 20:49:56 +02:00
Linus Rath c1e5cd6c8b fix: label the remaining settings icon buttons for screen readers
The filter rule reorder, delete and remove-condition/action buttons,
the filter, Sieve and folder editor close buttons and the attachment
keyword remove buttons had no label. The Sieve editor's line count now
uses an ICU plural, and the settings i18n test covers every pane.
2026-09-24 20:49:56 +02:00
Linus Rath 455608f9f0 fix: translate push setup errors and the open-link prompt
The Notifications pane shows a push setup failure as "<phase>: <message>"
with a translated phase but an English message, and the open-link
confirmation and the new-mail notification title were English too.
They now go through the locale store's t() (translate() in the
background task), with the technical detail passed as an argument.
2026-09-24 20:49:55 +02:00
Linus Rath 5801d6c97a fix: show a translated notice when the sign-in session expired
The sign-in screen showed the auth store's raw English error ("Session
expired", "Invalid username or password") as the notice title. It now
runs it through describeLoginError like every other sign-in failure,
which also learns to explain an expired session with the webmail's
login.session_expired copy. The back and close buttons of the sign-in
steps get screen reader labels, and the email placeholders use the
webmail's translated example address.
2026-09-24 20:49:55 +02:00
Linus Rath 771558d8c7 fix: translate the composer's leftovers and label its header buttons
Group recipient chips, the group size in suggestions, the upload
progress and the upload/send failure fallbacks were English or glued
together from a translated word and a number; they now use ICU
messages. The close, attach, schedule and sender-options buttons, the
recipient and attachment remove buttons and the From override switch
get screen reader labels (audit U8).
2026-09-24 20:49:55 +02:00
Linus Rath 9c0c35c7ba fix: translate the mail list and label its icon buttons
The filter sheet, the .eml import result, the selection count, the
folder totals and the offline sync banner were English literals or
built by gluing translated words to numbers. They now use t() with ICU
arguments and plurals; filter chips go through a "{field}: {value}"
message so translators control the order. The invitation trust
warnings used keys that no catalog had, so they are now literal t()
calls and harvested.

For screen readers (audit U8) the hamburger, select-all checkbox,
filter, sort and compose buttons, the selection toolbar, search and
chip clear buttons, and the close buttons of the move, tag, template,
identity, QR and action sheets get translated labels and roles.
2026-09-24 20:49:55 +02:00
Linus Rath ccdc4dee56 fix: add the template settings strings to the catalog
TemplateSettings calls the translate function `tr` because `t` is a
template variable there, and both the key harvester and the coverage
test only looked for `t('...')`. Ten of its keys were never added to
locales/rn/en.json, so they could not be translated and the test did
not notice. Both now also match `tr('...')`, and the missing keys are
added.
2026-09-24 20:49:55 +02:00
Linus Rath aab29da148 fix: translate the file sharing roles and label the files toolbar
The share sheet asked for files.share_role_* keys that no catalog has,
so Viewer, Editor and Manager stayed English everywhere; it now uses
the webmail's sharing.preset names and its "Share \"{name}\"" title.
The per-file menu, back and clear-selection buttons get screen reader
labels, and the "sharing is not available" error is translated.
2026-09-24 20:49:55 +02:00
Linus Rath d59c571d93 fix: translate the contacts screens
The contact list, detail and edit screens, the group screen, the
import, picker and tag sheets and the contacts drawer were written in
English, including the activity panel ("Unknown", "Upcoming events"),
alert titles and every form label and option. They now go through
t(), reusing the webmail's contacts.* keys where the meaning matches,
with ICU plurals for member, contact and import counts instead of
`contact${n === 1 ? '' : 's'}`. JSContact context, phone feature,
personal-info and gender values on the detail screen are shown with
their translated labels instead of the raw keys.

Icon-only buttons on these screens (back, edit, more, bulk actions,
remove, close, clear search) get screen reader labels, and chips,
checkboxes and collapsible sections report their state.
2026-09-24 20:49:55 +02:00
Linus Rath cb5c152cb3 fix: name the settings switches for screen readers
A settings switch had a role and a checked state but no label, so
TalkBack only announced "switch, off". A switch inside a SettingItem now
takes that row's label, and the switches outside one (filter rules,
expanded view, template and sidebar app editors, filter rule options)
get their visible label passed explicitly.
2026-09-24 20:49:55 +02:00
Linus Rath 6e6fd95e5e fix: translate the contacts and content settings
The Contacts and Content & Senders panes, the swipe behaviour row in
Layout, the device time zone option in Calendar and the identity editor
placeholders were written as English literals, so they stayed English
in every language. They now go through t(), reusing the webmail's
settings.contacts, contacts.address_books and
settings.email_behavior.trusted_senders keys where the wording matches.
Counts use ICU plurals instead of `contact${n === 1 ? '' : 's'}`, and the
address book icon buttons get screen reader labels.

The settings i18n test now parses each pane with the TypeScript compiler
and also flags JSX text, alerts and accessibility labels, and covers
the newly cleaned panes.
2026-09-24 20:49:55 +02:00
Linus Rath 8af163a6a3 fix: show today and the now-line in the calendar time zone
The calendar placed events by the calendar time-zone setting, but
"today", the now-line and the "Today" button still went by the device
clock. With the setting away from the device zone, the grid could
mark yesterday as today, draw the now-line hours off and jump to the
wrong day.

- the views, the agenda's "Today"/"Tomorrow" headers and today anchor,
  the selected day, the scroll window's start and the "Today" button
  use `displayNow`, `isDisplayToday`, `isDisplayTomorrow` and
  `displayNowMinutes`
- a long press at an hour already hands the editor a calendar-zone
  time, which it converts when it saves
- changing the setting re-buckets the days and reloads the loaded
  range, since the server reads floating times and the range bounds
  in that zone
2026-09-24 20:40:51 +02:00
Linus Rath b7f711050e fix: translate the calendar import sheet 2026-09-24 20:34:35 +02:00
Linus Rath 9282ee6d72 fix: open the event a calendar link points at 2026-09-24 20:34:35 +02:00
Linus Rath a8e6f6e8c4 fix: keep event and task times when the calendar time zone is not the device's
With the calendar time-zone setting away from the device zone, the
event and task editors showed device-local times but saved them
labelled with the calendar zone, so opening an event and saving it
unchanged moved it by the difference between the two zones.

The calendar now works in display dates like the webmail
(`toDisplayDate` / `fromDisplayDate`): Dates whose local fields read
as the wall clock in the calendar zone. They are the identity while
the calendar follows the device.

- `getEventStartDate` / `getEventEndDate` return display dates, so the
  views, the detail sheet and the editor show calendar-zone times
- the editors convert back through `eventTimeFieldsToSave` and
  `buildTaskEditorChanges`: a start, end or due the user left alone is
  saved exactly as stored, a changed one is converted into the zone the
  event or task lives in (new, floating and formerly all-day ones take
  the calendar zone), and durations count whole days as calendar days
  in that zone
- `getTaskDueDate` stays the real instant for reminders; the tasks
  sheet uses `getTaskDueDisplayDate`, `displayNow` and
  `isDisplayToday`
- wall clocks are parsed from their digits, so one inside the device's
  own DST gap still converts right
- duplicating an event keeps its wall clock in its own zone instead of
  reading the device time
2026-09-24 20:34:35 +02:00
Linus Rath b7a5953255 fix: ask for both neighbouring conversations in one request
When the pages beside the open message were let go, each asked for its
conversation on its own, so every open sent two `Thread/get` requests
for its neighbours.

Conversations asked for in the same tick for the same account now go
out together: `getThreadsHeaders` takes several thread ids in one
back-referenced Thread/get → Email/get, and the cache batches its
callers into it. A conversation the server does not know, or a failed
request, still falls back to the copy held from before.

`getThreadEmails`, which downloaded every member's full body, had no
callers left; it is removed and its tests moved to `getFullEmails` and
`getThreadsHeaders`.
2026-09-24 20:29:41 +02:00
Linus Rath 0cee422373 fix: stop reloading an invitation when the message is marked read
The invitation banner loaded the .ics whenever it was handed a new
message object, so marking an invitation read or starring it
downloaded and parsed the calendar part again (and fetched its raw
text when the method had to be read from it).

It now loads once per message, account and calendar part
(`calendarInvitationKey`), and reads the current message only when the
load runs.
2026-09-24 20:29:41 +02:00
Linus Rath 644443193c fix: drop the mail the viewer held in memory on sign-out
The viewer's message cache, the rendered documents of their bodies and
their measured heights stayed in memory after signing out or removing
an account. Their keys name the server and login, so no other account
saw them, but a signed-out user's mail should not stay around.

Signing out (one account or all) and removing an account now clear all
three. A load still in flight when the cache is cleared no longer
stores what it brings back.
2026-09-24 20:29:41 +02:00
Linus Rath d2066dad07 fix: act on the exact row picked in "All folders" and tag lists #1082
An "All folders" search or a tag view holds rows of the own and the
shared accounts, and Stalwart hands out ids per account, so an own and a
team message can share an id. The list still named its rows by bare id:
selecting one of such a pair selected both, a batch action hit both, and
a swipe or single-row action took whichever came first, in the other
account.

The list now names every row by `rowKeyOf` (its id, scoped by its
account in a list spanning accounts; the bare id in a folder, so nothing
changes there) end to end:
- FlatList keys, row presses, swipes, selection and select-all
- thread expansion of a selection (`expandThreadSelection`)
- every store action the list calls: read, unread, star, pin, the
  per-account keyword batch, move, archive, delete and spam, and the
  rows they drop or keep in view
- the viewer's pages, which now come only from the rows of the opened
  message's account (`listRowsOfAccount`), and its mark-read, which
  goes through the store when the list holds that account's row

A key whose row is no longer loaded is ignored rather than guessed onto
the open folder's account. The unified inbox already keys its rows by
account and needed no change.
2026-09-24 20:28:43 +02:00
Linus Rath dd0969d48b chore: drop translations of app strings the calendar agenda no longer uses 2026-09-24 20:27:58 +02:00
Linus Rath 96bffa51e8 refactor: check only the days a timed event spans for the all-day strip
`buildTimedFullDayWeekSegments` tested every event against every day it
was given. That was fine for the paged week, but the scrolling week and
day grids hand in months of days. It now tests each event only on the
days it spans, with the same `isTimedEventFullDayOnDate` check, so the
strip and the timed grid still agree on every day (DST days included).
The time grid's own copy of this is gone.
2026-09-24 20:25:41 +02:00
Linus Rath 5434656e03 feat: bring a tapped reminder's day into view in the calendar 2026-09-24 20:25:41 +02:00
Linus Rath de3d697aa9 feat: scroll the calendar week and day views sideways #759
The week view showed one week and only the arrows moved it, and there
was no day view at all (a "day" default view opened the agenda). With
free scrolling on (the default) both are now one strip of day columns
that scrolls sideways, as in the webmail (15a5497a):

- the week view shows seven columns and snaps to single days; the new
  day view shows one column and moves a day per swipe; the title
  follows the first day in view and the arrows step from it
- reaching either end adds days and loads only their events; days added
  at the start keep the columns on screen in place
- the day headers and the all-day strip follow the sideways scroll on
  the native thread; all-day bars are cut per week (per day in the day
  view) so each part carries its title
- columns are windowed and lay out their events once per change

The day view is offered as a default view again. With free scrolling
off, or in right-to-left layouts, the week view pages as before and the
day view shows a single day.
2026-09-24 20:25:40 +02:00
Linus Rath caf61ece79 feat: scroll the calendar month view continuously #759
The month view showed one month at a time and only the arrows moved it.
With the new "Free scrolling" setting (on by default, the webmail's
`calendarFreeScroll`) it is one continuous list of weeks, as in the
webmail (15a5497a, ae403b06):

- the month under the upper part of the grid is drawn at full strength,
  names the title and is where the arrows step from; the first day of
  each month carries the month's name
- reaching the top or the bottom adds weeks there and loads only their
  events; weeks added above keep the rows on screen in place
- rows have a fixed height and snap to whole weeks, so the list stays
  windowed and navigation lands exactly on the month

Turning the setting off brings back the paged month and a fixed 30-day
agenda. The week view still pages.
2026-09-24 20:25:40 +02:00
Linus Rath 71522b9b92 feat: scroll the calendar agenda endlessly instead of a fixed 30 days
The agenda listed 30 days from the current date and the arrows paged
through it 30 days at a time. It now keeps a window of days that grows
as the user scrolls, like the webmail's agenda (60e05c00):

- reaching the end loads further ahead, doubling that side up to a year
- "Show earlier events" loads the past without moving what is on screen
- "Today" and the arrows scroll to their day while it lies inside the
  window and start a fresh window there otherwise; the title follows the
  day in view
- the "Today" row only anchors the list when today was actually fetched

Growing the window fetches only the new days: `extendRange` in the
calendar store loads the missing part and merges it into the loaded
window (up to about three years) instead of refetching all of it.

Navigation now goes through one focus for every view, so the week
view's arrows move the grid (it kept showing the selected day's week)
and the month arrows carry the selected day along.
2026-09-24 20:25:40 +02:00
Linus Rath 67935d2edc feat: translate the app-only strings into all 26 languages
Every language now carries its own `locales/rn/<lang>.json` with the
roughly 600 strings the webmail catalog does not have, so settings,
sign-in, files, calendar and push screens no longer fall back to
English.

Terms and the form of address follow each language's vendored webmail
catalog, and plural messages use the language's own CLDR categories.
Wording that had no precedent in the catalog would benefit from a
native speaker's review.
2026-09-24 20:25:40 +02:00