Releases: Aevorine/Aevistle
Release list
Aevistle 0.1.31
Full Changelog: v0.1.30...v0.1.31
Aevistle 0.1.30
Full Changelog: v0.1.29...v0.1.30
Aevistle 0.1.29
Full Changelog: v0.1.28...v0.1.29
Aevistle 0.1.28
Aevistle 0.1.28
Two things at once: the four screens you spend the most time on were rebuilt
around how much room the text actually needs, and another round of hunting for
defects that run cleanly and do not do what they say. This time one of them was
sending mail against an instruction not to.
The screens
The type scale had collapsed
Three of the size tokens — "secondary", "body" and "input" — were all set to
16px. So metadata was the same size as body text on every screen, there was no
hierarchy to read, and the whole app looked like a wall of large print. There is
a real ladder now: 24px screen title, 20px for the one value a card exists to
show, 18.67px section name, 16px body and every input, 14px secondary, 12.5px
auxiliary. Inputs stay at 16px on purpose — anything smaller makes a phone zoom
in when you tap a field.
The new-reminder screen had a second problem on top of that: it raised the
smallest size to 16px for its entire subtree. Measured, that one override was
costing the message box 44px of height.
There is a reason this had survived several attempts to fix it. The layout check
in this repository hard-asserted that no text may be smaller than 15.9px, so any
change that introduced a smaller size failed the check and was reverted. That
floor is graded now — body ≥15.9, secondary ≥13.9, auxiliary ≥12.4 — and a
violation says which rank it broke.
The inbox: 150px of a 360px screen was not text
A mail row spent its width on a checkbox, an unread dot, two 38px buttons and
three gaps, and left the subject 146px — about nine Chinese characters — which
it was then forbidden to wrap. Everything after that was an ellipsis.
The two row buttons have moved into the swipe gesture that already existed, and
the checkbox now appears only while a selection is actually in progress. The
result, measured at 360px: 20px of furniture, 272px of subject, and a
22-character subject rendering on two full lines with nothing cut off.
Nine rows on screen at once was the original target and it is not met with a
two-line subject: two lines plus a metadata line and padding is a 92px floor,
and nine rows in the available space needs 70.8px. It is six rows with a
two-line subject, eight with a short one, and nine on compact density — which
is a setting you already have. Legibility won that trade deliberately.
New reminder: the message box now has 85% from the moment it opens
It always could reach 85%, but only after you tapped into the body and the
address block folded itself away. Before that it was 71.5% — or 52% with more
than one account configured, because the account picker added another field. A
promise kept after the fact is not kept.
It opens folded now, with a one-line summary bar that reads as a tappable field.
Measured: 85.5% on a 360×800 phone, 92.3% on an 820×1180 tablet. The account
picker lives inside the block that is not rendered, so the multi-account case is
the same number rather than 52%.
The 85% had never actually been checked. It was documented at length in the
layout script and never asserted; the number it computed divided by the window
height rather than the screen's own, and the script never simulated a phone at
all. It does both now, on the state the screen opens in, and fails below 85%.
Verification codes and login links
The reported problem was that a login link ran outside the card. It was not the
link — that was already trimmed with an ellipsis. It was the button row: it had
no wrapping rule, and four labelled buttons need 447.5px in a 284px card, so
180px hung off the edge and dragged the whole list into sideways scrolling. The
buttons are icons on a narrow screen: 128px, one row, nothing overflowing.
The code itself is 20px rather than 24px (28px on a wide screen), and the full
login URL is now printed on the card, wrapped and clamped to two lines. It
previously existed only in a tooltip you had to hover to see.
Found while measuring
- Every list screen reported 4px of horizontal overflow you could not scroll to,
at every width, from a divider that extended past the page gutter. - Account labels were clipped with no ellipsis, because the element that carries
the ellipsis rule could never shrink. - Once the metadata line stopped wrapping, "2 min" broke across two lines —
41.3px of row height for a timestamp. - The list-density setting moved every list in the app except the mail list.
- The 44px minimum touch target applied only below 560px, so an Android tablet
at 820px — which the app does treat as a touch device — was given 28×26px
formatting buttons.
The defects
Send conditions did nothing at all on Android
This is the serious one. The Android scheduler never looked at a job's
conditions. Not one line of the native code read the field. So "only send if
they have not replied", "only if this file exists", "only inside these hours" —
all six kinds — were set, saved, synced to the phone, and then ignored. The mail
went out regardless, and nothing in the activity log even mentioned that a
condition had been considered.
All six are evaluated on the device now, including "has not replied", which
reads the same downloaded mail the inbox already keeps. A condition this build
does not recognise blocks the send with a readable reason rather than passing.
And "has not replied" did nothing for scheduled sends on the desktop either
Found while fixing the above. The desktop scheduler was never given the mailbox,
so it reported that condition unanswerable — and an unanswerable condition
deliberately sends rather than holding mail back on a guess. The effect was that
the condition worked when you pressed "Run now" and did nothing whatsoever for a
scheduled send, which is the only case it exists for.
Sharing to Aevistle opened an empty message
Following a mailto: link or using "Send to" with the app closed opened the
compose screen with every field blank. The shared content was written into the
draft before start-up had finished, and start-up then replaced it with whatever
had last been left in the compose box. On Windows this happened every time.
A missed reminder was never paid on Android
A reminder due while the phone was off was dropped rather than sent late, and the
schedule row kept showing a send time in the past that never advanced. It is
paid now, exactly once — the guard against sending it twice is stored on disk,
because a worker that is killed immediately after sending must not forget.
A skipped send was reported as a failure
When a condition held mail back, the activity log showed a red "Scheduled send
failed" with an empty detail, and discarded the one useful fact: which condition
said no. It is a warning carrying the reason now. On Android, a skip that
happened while the app was closed produced no log line at all.
Four settings that could be read but not set
All four were in the settings file, had a default, were read by code that
changed behaviour, and had no control anywhere in the app:
- Copying a verification code to the clipboard automatically — permanently
on, so the app overwrote your clipboard every time it recognised a code, with
no way to decline. - Keeping a history of unsent drafts — permanently on, writing draft text to
disk with no way to decline. - The size limit and retention period for downloaded mail — 500 MB and 90
days, with nothing in the interface that touched either.
All four are in Settings → Privacy now.
Smaller
- A reminder moved off a holiday could be scheduled into the past, so the
preview said it would not be sent and the desktop then sent it immediately. - Jobs saved by an older build never caught up after a missed run, while an
identical new one did, because two halves of the same rule disagreed about a
missing value. - Dragging a series that had missed a run moved it one day further than asked.
- Two English sentences appeared in the activity log in all six languages.
Housekeeping
216 translated strings were removed — 36 helper sentences, across six languages,
whose interface element had stopped asking for them in an earlier round. The
strings stayed behind, translated, for text nobody could see. There is now a
check for that, in the direction nothing checked before: a key that no code path
can reach fails the build.
Two new checks guard what this release fixed. One holds the whole share chain —
the Android intent filters, the Windows registry entries, the command-line
parse, both buffers and the start-up ordering — because every break in it is
silent and looks like the feature was never built. The other is the graded type
floor and the 85% assertion described above.
A note on Windows and the system share sheet
Aevistle can be your default mail application on Windows, and it appears in
Explorer's "Send to" menu. It does not appear in the Windows 11 share panel, and
it will not: that requires the application to be packaged as MSIX, and this is a
conventional installer. On Android the share sheet works as you would expect —
long-press anything, choose share, and Aevistle is in the list.
Aevistle 0.1.27
Full Changelog: v0.1.26...v0.1.27
Aevistle 0.1.26
Aevistle 0.1.26
An audit release. Nothing here is a new feature; it is the result of going
looking for the kind of defect that does not announce itself — code that runs,
returns cleanly, logs nothing, and does not do the thing it says it does. Eight
were found, and every one of them had been shipping.
Two notification switches that could not affect anything
Settings has had "announce successes" and "announce failures" since early on.
Neither did what its label said, on either platform, in four separate ways:
notifyOnFailurewas read by nothing at all. Not one line of TypeScript
anywhere in the project consulted it. The desktop's failure notification
fired unconditionally, so turning the switch off changed nothing.notifyOnSuccesswas read only by the compose screen's own send button.
A scheduled send — the thing this application exists to do — never
announced success on the desktop, because the scheduler runs in the main
process and the main process had never been told the setting existed.- On Android, success notifications were wired to a field that has never
existed.SendWorkerasked each job fornotifyOnSuccess;ScheduledJob
does not define it and nothing has ever written it, sooptBoolean(…, false)
returnedfalseevery time. A scheduled send that worked has never once
raised a notification on Android, whatever the settings screen showed. - On Android, failure notifications ignored the switch too, at both of the
places that raise one.
All four are fixed by giving the two settings a route to the code that acts on
them: setDesktopPrefs on the desktop, alongside the two switches that had
exactly this problem before them, and syncJobs on Android, which the
background worker already reads its jobs from. The desktop's scheduled-send
notifications are also translated now, and name the reminder rather than
printing its internal id.
Two Android event streams that were never connected
onJobEvent and onInboxEvent are declared in the Android bridge, and both
subscribed to event names nothing in the Java plugin ever emitted —
notifyListeners was called for two unrelated things and for nothing else.
Subscribing succeeded, so from the web side nothing looked wrong. What it cost:
- a scheduled send completing while you were looking at the schedule screen
left its row saying "waiting to send" until you switched apps and came back; - mail found by the background sync did not appear until the app's own timer
came round, up to five minutes later.
Both ends are connected now. The workers publish through a plugin instance that
registers itself while a WebView is alive, and the emit is a no-op when nothing
is open — so the closed-app path still runs entirely on the queue-and-drain
mechanism it always did, and nothing new can be lost with the app shut.
A leftover debug override in the Inbox screen
const canUseInbox = Boolean(bridge?.syncInbox) || true /*TEMP-VERIFY*/
The || true made the condition constant. Two consequences, both quiet: the
browser preview, whose bridge has no receiving support at all, drew the whole
Inbox screen — sync button, account filter, bulk actions — none of which could
do anything; and the empty state that would have explained why became
unreachable code.
Fixed by separating the two questions that had been collapsed into one. Whether
the platform can fetch now gates only the sync button. Whether there is
anything worth drawing is a different question, and mail already in the store
is still shown — hiding real messages because more cannot be fetched would have
been the same class of bug facing the other way.
A dependency advisory that had been failing the build for weeks
npm run check ended in a red nanoid advisory that predated this work and had
been treated as permanent scenery. It was real and one line from fixed:
GHSA-2v37-7h3g-55p8, high severity, reachable at runtime through
sanitize-html → postcss. Pinned to a fixed version.
npm run check now exits zero.
402 dead translation entries
67 keys that nothing in the project references, across all six locales —
leftovers from screens that were reorganised (the verification-code list moved
out of the inbox and became its own screen; the export/import section went
away). Removed. Anything still in use would have failed the type checker, since
the translation key type is derived from the English table.
And a new gate, so the Android one cannot come back
The most interesting defect above is the third: Java reading a JSON field the
TypeScript side never writes. It cannot fail loudly — optBoolean has a
default, and the default is a perfectly ordinary value — so it produced a
feature that was off forever while every existing check stayed green.
check:native-fields now reads every field the Android sources pull out of
JSON and requires something in src/ to write it, with an explicit allowance
for the one field that legitimately comes from elsewhere (an OAuth token
response). It carries a self-test that injects the exact fault and requires the
check to go red.
Two things were looked for and not found, which is worth recording: there are no
settings fields that nothing reads (the two above were the last), and there are
no empty catch blocks in either language.
Aevistle 0.1.25
Aevistle 0.1.25
Mail arriving in a mailbox this app was watching produced no notification at
all. Not a poor one — none. On both platforms the only things that had ever
raised a notification were a scheduled send's result and a verification code,
so everything else landed in the inbox in complete silence, and on Android the
silence was total: the background sync that runs on the system's own schedule,
with the app closed, never notified about anything.
That is what this release is mostly about, along with two smaller things that
were reported at the same time — a copy button that failed on every Android
device, and two fields in the add-account form that were the same word twice.
Copying a verification code on Android said "copy failed"
It had never worked. The screen that exists so a code can be copied in one tap
was, on the platform where that matters most, a screen that found the code,
displayed it correctly, and then refused to copy it.
The cause is not in this app's logic. navigator.clipboard.writeText() — the
modern clipboard API, called from a real tap, on a page served over
https://localhost and therefore in a genuine secure context — is refused
inside an Android WebView. The asynchronous clipboard write goes through
Chromium's permission service, and a WebView has no delegate that can answer
for clipboard-write: the hook it does have covers audio, video, MIDI and
protected media, and nothing else. So the promise rejects, and the catch
around it did the only thing it could, which was to report a failure.
Every copy in the app now goes through one place that tries three routes in
order: the Android clipboard directly, through a new native method, which needs
no permission at all and is the API the platform actually intends for this; then
the web call, which is what Windows and the browser preview use; then a hidden
text field and the older execCommand, so a build running somewhere neither of
the first two reaches still copies. That covers the codes screen, the sign-in
link, the pairing code, the calendar subscription URL and the remote-control
configuration — all of which were failing on Android in the same way, and two of
which were worse than failing: they announced "copied" whether or not anything
had reached the clipboard, which on Android was every single time.
Automatic copy of a code while waiting for one has therefore also never worked
on a phone. It does now.
New mail is announced, and the notification takes you to it
There is a new switch in Settings → Notifications, on by default, next to the
verification-code one. What it produces is a notification carrying the sender,
the subject and the first line of the message, wearing the app's own icon, and
tapping it opens that message.
What counts as new is deliberately narrow, because the obvious version of this
feature fires constantly for the wrong reasons. Three rules:
- Nothing from the first sync after launch. That sync discovers the whole
mailbox, and every message in it is "new" to a process that has just started.
Without this rule, opening the app means a burst of notifications about mail
you read yesterday. - Nothing already read elsewhere. A message flagged as seen was read in
webmail or on another device, and arriving here is not an event. - Nothing older than half an hour. A mailbox that has been offline for a
week catches up in one sync. Those messages are new to the cache and old to
the world, so the age is judged from the message's own date rather than from
when we happened to hear about it.
Quiet hours suppress it, which is a deliberate difference from the code
notification: someone waiting for a code at 02:00 is waiting on purpose, and a
newsletter at 02:00 is exactly what a nightly window exists to hold back.
On Windows the click raises the window — restoring it first if it was minimised,
and opening one if the app was closed to the tray — and then opens the message.
Every notification the desktop raises now carries the app icon and does
something when clicked, including the scheduled-send failure notice, which
previously had neither.
On Android there is more to it, because the app is usually not running:
- The background sync notifies. The fifteen-minute WorkManager pass now
compares what came back against what it already had and raises a notification
for genuine arrivals. This is the change that makes the feature exist at all
on a phone. - Notifications wear the app's mark. The small icon was the platform's
generic sync glyph, which is what every background sync on the device uses;
it is now a purpose-drawn envelope, tinted with the app's own accent, with the
launcher icon beside it. - Tapping opens the message. Including from a cold start, where the tap is
what launches the app and there is no page to deliver an event to — the id is
parked and collected when the app is ready. - Several arrivals collapse into one group rather than stacking up as
separate rows. - A code notification has a Copy button. The whole point of that
notification is not having to switch apps, and it previously stopped one step
short: you could read the code and then had to type it. The button writes it
to the clipboard from the shade and dismisses the notification, without
opening the app.
One limit, stated plainly: with the app fully closed, the background pass
reports new mail, not a verification code. Recognising a code means reading
the message body, and the background sync fetches headers only. Codes are still
recognised and announced whenever the app is running.
"Your name" and "Account remark" were the same word twice
The add-account form had two fields that both asked for a name and meant
opposite things. Sender name goes into the message header and is what the
person receiving your mail reads; Account alias never leaves the device and is
what this app's own account list calls that mailbox. Written as "你的名字" and
"账号备注" — "Your name" and "Display name" in English — nothing distinguished
them, and the layout made it worse: one sat in step 1 and the other was folded
away inside "More settings", so the two were never on screen together to be
compared.
They now share a row, each with a note on its label line saying where its value
shows up, and directly underneath is a line printing both as they will actually
appear — Recipients see: 张三 <me@gmail.com> · Listed here as: Gmail. The note
is on the label line specifically so it survives on a phone, where the
stylesheet culls the ordinary hint text under an input. The email address moved
to a row of its own to make room, which it had earned anyway: it is the largest
control in the dialog and the field every other one is derived from.
Also
- Two new gates in
npm run check. One holds the new-mail rules to the same
window and the same three tests on both platforms — the decision exists twice,
in TypeScript for the app and in Java for the background worker, and two
implementations of one rule drift silently. The other requires the handful of
strings the Android background worker words for itself to be translated in
every locale, for the same reasoncheck:i18nexists on the other side of the
bridge: a missing translation there does not fail, it quietly falls back to
English.
Aevistle 0.1.24
Aevistle 0.1.24
Typing on Android did not reach the app. That is the plain version of a report
that arrived in two halves — "I enter the email address and nothing fills
itself in", and "I type in one box and the box I filled a moment ago goes
empty" — which sounded like two bugs in the add-account form and were one line
of configuration underneath it.
android.captureInput was on. It is Capacitor's hook for hardware keyboards
and barcode scanners, and it works by handing the Android keyboard a
non-editing input connection: there is no text buffer for the keyboard to
compose into, so the system falls back to raw key events, and anything it
cannot express as a keycode — a predicted word, any Chinese input, an emoji —
arrives by a route that Capacitor handles with document.activeElement.value = … + '…'. That assignment writes straight into the page. It fires no input
event. React therefore never learned a key had been pressed: the box looked
filled, the app's own state was still empty, and the next time anything at all
re-drew — one keystroke in the password field, a switch, the seconds counter on
a connection test — every field snapped back to the empty value the app
believed in. Whichever box you touched, the others cleared, which is exactly
what was described. It is also why the address filled nothing in: automatic
configuration runs off that same change event, so the provider, both servers,
both ports, the encryption and the username were never derived at all. Nothing
in the Android code was reading key events, so the setting was buying nothing
and costing every text field in the app. It is off, documented where it lives,
and npm run check now fails if it comes back — including if it comes back
only in the copy inside the APK, which is the form of this that would otherwise
be fixed in the repository and still broken on the phone.
With typing working, the add-account screen was rebuilt around what it actually
does. The address is the form: every keystroke in it re-derives the provider,
the send and receive servers, both ports, the encryption and the username. So
it goes first, in the largest control in the dialog, and directly underneath it
is a panel saying what it just decided — the provider it matched, and the two
servers it wrote, printed rather than described. That panel is also the only
honest way to present a guess: an address on a domain no preset knows still
gets values, derived from the smtp./imap. convention almost every mail host
follows, and a guessed host looks exactly like a known-good one right up until
the connection test fails. It now says so, in the warning palette, with the
values it guessed on screen.
The credential moved ahead of the server settings. Those two boxes hold the
only thing on the screen a person has to go and fetch from somewhere else — an
app password from the provider's own site, or a round trip through the browser
for OAuth2 — while everything below them was filled in automatically the moment
the address was typed. Putting the machine's work above the person's meant
scrolling past four correct fields to reach the one empty one. The password
gained a show/hide control, which a phone needs more than a desktop does: it is
sixteen characters of provider-generated noise, and getting it wrong otherwise
costs a full connection test to find out.
One layout fault turned up while checking that: a domain with no preset is
printed inside a sentence — "no preset for <domain>" — and a domain is a
single unbreakable token. A long corporate one needed about 295px of the 182px
that column gets on a 360px screen, and with nothing to break at the text ran
straight out of the panel and gave the whole dialog a sideways scrollbar. It
wraps now.
Both READMEs now answer the Google sign-in error, because it is not a fault
anyone can fix from inside the app. "Aevistle has not completed the Google
verification process — Error 403: access_denied" is the state of this project's
Google Cloud registration: IMAP and SMTP accept only the https://mail.google.com/
scope, Google classes that as restricted, and a restricted-scope app stays in
testing — admitting only hand-listed test users and refusing everyone else with
that 403 — until it passes a verification that requires an independent CASA
Tier 2 security assessment, renewed annually. App passwords are unaffected and
remain the supported path for Gmail; the README says so, and says what building
with your own client id would take for anyone who wants the button instead.
Three numbered steps, then two blocks marked optional. The server settings stay
expanded on every screen size rather than folding away into an "advanced"
disclosure — a wrong port is the commonest reason a send fails, and a port you
cannot see is one you cannot fix. The display name is now marked optional,
because it always was. The address, username, password and host boxes tell an
Android keyboard not to capitalise or autocorrect what goes into them, which is
how Me@Gmail.con was becoming a plausible thing to end up with — and since
the domain drives the whole of automatic configuration, a helpfully corrected
one silently meant no provider matched.
Aevistle 0.1.23
Aevistle 0.1.23
The desktop sidebar carried a line under the logo saying what the app does —
useful the first time, and then permanently in the way of the one thing a
navigation rail is actually for. It is gone, and the extra vertical room let
the rail narrow further: from 208px down to 185px, re-measured the same way
the last cut was — real scrollWidth/clientWidth against every nav label in
all six languages, not estimated. That first measurement missed something the
first cut didn't have to account for: unread and scheduled-count badges. A
mailbox with hundreds of unread messages was pushing "Inbox" past its column
and clipping it, in every language, because the badge digits were never part
of the width budget. Badges now cap their display at "99+", the same
convention every mail and chat app uses, and the rail is re-verified clipping-
free with that in place.
Adding a mail account on a phone was one long form — sixteen-odd fields in a
single column, the address box no bigger than any other field despite being
the one thing everybody actually has to type. It is now grouped into named
sections: address first and enlarged, since that's the field that drives
everything else; server settings next, still fully visible because a
disabled-test-button explanation has to point at fields you can actually see;
optional extras (label, group, reply-to, the two advanced switches) folded
under a "more settings" disclosure that starts open on desktop and closed on
a phone; receiving last. Fixing this also surfaced a genuine React bug: a
native <details> element's open attribute does not reliably clear itself
through a state update, so the "more settings" panel could stay expanded
across different accounts' edit dialogs. It is now synced with a ref instead
of trusted to the attribute.
A pass through every long-winded hint, banner and tooltip in the app —
desktop and Android both, including the handful that survive the phone's
usual rule of hiding explanatory text — cut the ones that were saying the
same thing twice across two different screens (the Windows Defender firewall
notice, the Microsoft OAuth2 cutoff explanation, the four port/encryption
hints) down to one clear version each, and trimmed a dozen more that spent a
sentence explaining why before ever getting to what to do about it. Nothing
that states a port number, a deadline, a URL, or a promise about what does or
doesn't happen to your data (server mail, plaintext passwords, automatic
sending) was touched — those stayed exactly as specific as they were.
Also: the crash-recovery screen — the one that appears if a single screen
fails to render — was hard-coded in English regardless of the app's language
setting. It now reads in whichever of the six languages you're using, same as
everywhere else, and still says the one thing that screen exists to say:
your accounts, scheduled sends and mail are untouched by a rendering crash.
Nothing about how mail is scheduled, stored, sent or encrypted changed.
Aevistle 0.1.22
Aevistle 0.1.22
The desktop and tablet sidebar was sized to the longest label any of the six
languages needed — Spanish's "Bandeja de entrada" — which meant the other five
languages carried 30-40px of rail they never used. Spanish and French had a
shorter, equally standard word for the same tab ("Recibidos", "Réception"),
and giving that back dropped the true minimum from 244px to 208px with zero
truncation, measured against every label in every language, not estimated.
- Sidebar narrowed 244px -> 208px, handing that width to whichever screen is
open next to it. Verified with the real renderedscrollWidth/clientWidth
of every nav label, in all six languages including right-to-left Arabic, and
the icon-only collapsed rail is unaffected. - Spanish and French inbox tab: "Bandeja de entrada" -> "Recibidos", "Boîte de
réception" -> "Réception" — shorter, standard alternatives used by other
mail clients, not an abbreviation invented for this fix.
Two other things were reported as missing and turned out already to be
working once checked against this build rather than an older install:
account setup fills in the SMTP/IMAP host, port, encryption and provider the
moment an address is typed — no button press — on both platforms, since the
form is the same code either way; and the update card's check, signed
download and install flow has been wired end to end on Windows and Android
since a much earlier release. If either still looks broken on your device
after installing this build, that is a real bug and worth reporting again.
Nothing about how mail is scheduled, stored, sent or encrypted changed.