Veyra 1.26.30

Release pack published: 27.07.2026

Windows

Android

iOS

Linux

macOS

Release notes

Release 1.26.30

Cancelling a file transfer follows the same badge rule
- A cancelled file transfer left a red bubble the chat badge did not count. cancel_outbound_attachment_local closed the queue row as delivered while repainting the message as failed, so the bubble showed an error with a retry button and the chat row showed nothing. The 1.26.29 rule — *the badge counts exactly the messages whose bubble shows as failed* — now holds in both directions, and the cancel splits on the one fact that matters: - The envelope already reached the peer (the usual case: a big file cancelled mid-upload long after the caption arrived). Only the file was cancelled, so nothing failed: the bubble keeps its delivery status, the attachment card says "cancelled" on its own as before, and the queue row is closed because no work is left on it. Previously the bubble was left alone here too, so this case is unchanged in practice — it is now unchanged *by rule* rather than by accident. - The envelope never made it. The message genuinely did not arrive, so the queue row is marked failed alongside the bubble. The badge counts it, and the two ways down are the same as everywhere else: the retry button on the bubble, or deleting the message.
- Retrying a cancelled transfer could resume the upload the user had just cancelled. outbound_attachment_chunks_pending calls itself the single source of truth for "may chunks flow" and covered declined, paused and completed transfers — but not a sender-cancelled one, whose row keeps status = 'available' and receiver_decision = 'accepted'. A retry (reachable exactly when the bubble was red) therefore re-armed the chunk stream. Terminal transfers are now excluded from the gate: a retry re-offers the envelope and never restarts the cancelled file.

---
Version history

Version 1.26.29
Sending spins, and the chat failure badge can always be cleared
- A message being delivered now shows a spinner instead of a static clock. The release UI collapsed every pre-sent phase into one calm clock (1.26.10), but a clock that never moves reads as "stuck" — and it was the same icon as the genuinely parked deferred state, so the two were indistinguishable. Any phase where work is actually happening — preparing, route hunt, dispatch, retry ladder — is now a spinner. The clock is left to mean exactly one thing: the message is parked and waiting for the recipient to appear. The debug UI is unchanged apart from using the same icon it already did.
- The (!) badge on a chat row could stick forever with nothing to click. It counts failed outbox rows, and clearing the queue on the Network page is the only thing that produces them — deliberately, so each undelivered message can still be retried from its bubble. Two kinds of row have no bubble, and therefore no retry button and no way to ever bring the badge down: - Edit/delete propagation rows. save_outbound_mutation writes an outbox row with no messages row at all, so deleting a message and then clearing the queue left a permanent badge — the reported "I deleted the problem message and the marker stayed forever". Clearing the queue now removes these rows instead of marking them failed: the user asked for the queue to stop, and for a row with no interface the honest outcome is removal, not a mark nobody can act on. - Rows whose message was deleted. Deletion tombstoned the bubble and left the queue row behind. Deleting a message now drops its outbox row — a tombstone has no body left to deliver, and the delete still reaches the peer as its own mutation envelope with its own row. The badge itself now counts only rows that still have a live, non-deleted message, so every badge already stuck in the field clears on upgrade without a migration, and any future row of this class cannot raise one. The rule it encodes: the badge means "undelivered messages you can see and retry", and the two ways down are the retry button and deleting the message. Both pinned by tests that fail on the previous behavior.

Version 1.26.28
"Keep the screen on" toggle
- Interface → Screen → «Держать экран включенным» (mobile only, present in both debug and release builds). While the app is open the display will not switch itself off; minimising it lets the screen sleep as usual, because both platforms hold the request against the foreground window rather than a global wake lock. - Android needed no native work: MainActivity already had a dedicated setManualKeepScreenOn slot and a setKeepScreenOn JavaScript-interface entry that nothing called, with onResume re-applying the flag. The preference is pushed at the bridge on startup and on every change. Its own slot means it neither clobbers nor is clobbered by the call, chat-activity and backup keep-screen-on requests. Default off — Android never held the screen awake outside those three. - iOS already disabled the idle timer unconditionally in the foreground, so the toggle defaults to on there and the out-of-the-box behavior is unchanged; what is new is the ability to switch it off. Application setup, the Resumed window event and notify_app_resumed now apply the stored preference instead of a hard-coded true, and the AGENTS.md foreground screen-awake rule is updated to match. - The preference lives with the other UI preferences (keep_screen_on in app_meta, carried by get_ui_prefs / set_ui_prefs), so it survives restart and is readable before unlock. Labels and hints in all 20 locales. - The section is hidden on desktop, like the existing tray/autostart section: holding a desktop display awake needs per-OS work (Windows SetThreadExecutionState, macOS IOPMAssertion, Linux screensaver inhibit) that is not in this change.

Version 1.26.27
Inbound messages sit where they were written
- A message delayed in transit is now shown in the place it was written, not in the place it arrived. Since 0.9.562 an inbound row was ordered by local received_at, which meant a message that took a detour appeared below messages written after it — permanently, because arrival order cannot be undone. The rule ratified 2026-07-27 replaces it: > An inbound message takes the position where it was written, but never above > a message the user wrote themselves, and only while the delay stays within > 5 minutes; a later arrival keeps its arrival position. The own-message floor is what keeps the transcript honest. Without it a message delayed past the user's own reply would slot in above that reply and read as if the reply had been written knowing it — the exact case that made the plain "sort by written time" version unusable. Only OWN messages form the floor: inbound rows must stay reorderable among themselves, otherwise a pair that arrived swapped could never be put back in order. The 5-minute window covers the realistic live-transport spread (the bridge receipt bet is 90 s, the first outbox ladder tiers 30 s and 5 min); a store-and-forward drain hours later still lands at the bottom of the chat where the user will see it, which is what 0.9.562 was protecting.
- The position is decided once, when the row is written, and stored in the new messages.display_at column (schema v27) — it depends on what was in the chat at the moment of arrival, which no later query can reconstruct. Five queries that each rebuilt the old sort expression inline now read one indexed column, and a message never moves again after insert. The migration backfills display_at with the old expression verbatim, so every existing chat opens in exactly the order it had before the upgrade.
- The window also serves as the clock check. A contact whose clock is wrong — a dead RTC, a bad time zone — produces an implausible delay, or a message "written" in the future, and simply falls back to the arrival position instead of scattering the chat across days.

Version 1.26.26
An earlier message no longer waits behind later ones
- A message written earlier could hang for minutes while messages written after it were delivered at once. Traced on the Samsung with the field log: three messages went into the same chat at 04:31:12, 04:31:39 and 04:32:33 — the first and the third were confirmed within ~17 s, the second only at 04:35:10, after two more re-sends. Two independent defects stacked up. - The realtime bridge node queues a frame for the recipient's C-HTTP pull for REALTIME_BRIDGE_FRAME_TTL_SECS = 60 s, but the client pulled the primary bridge node only when its registration renewal was due. With a 90 s registration TTL and a 30 s refresh tick that renewal fires every *other* tick, so two pulls could be 60–80 s apart — longer than the frame lives. A frame submitted right after a pull expired unread on the node and was dropped silently; the sender learned nothing and only re-sent after its 90 s receipt window. Standby nodes were already drained on every tick; the node actually carrying the traffic now is too, so the pull cadence (30 s) stays strictly inside the frame TTL. - While waiting for that receipt the row sits in mailbox_wait, and a peer wake — a new message to the same chat, or any route that just came up — skipped it entirely. So the newly written message took the fresh route and arrived first, while the older one kept waiting for its own timer. A peer wake now re-includes rows parked on that short bridge bet (≤ 120 s), because they hold no durable copy. Real mailbox deposits (HTTPS transit 3 min, store-and-forward lease) have longer waits and stay parked, so the battery contract of the 2026-07-09 deposit-once arc is unchanged.
- Messages to one peer now enter the delivery path in the order they were written. A drain used to hand every row to send_one concurrently, so within one batch a later message could win the race to the transport — and the recipient orders inbound messages by arrival, which made the swap permanent. Rows are now chained per peer in created_at order; different peers still run in parallel. The chain link opens as soon as a row has been through the route ladder, so attachment chunk streaming never blocks the text message typed behind a large file, and a row stuck on a dying route hands over after 15 s instead of holding the chat hostage.

Version 1.26.25
Bigger menu close button on desktop
- The hamburger menu's close button was too small on desktop. The box was 36×36 with an 18px glyph, while the phone override had been raised to 48×48 with a 24px glyph in 1.26.8 — so only the desktop side kept the small version. Desktop is now 40×40 with a 22px glyph, matching the weight of the back arrow (22) and the hamburger (20) instead of being the smallest icon in the chrome. Mobile is untouched, which was measured rather than assumed: 48×48 / 24px there, 40×40 / 22px on desktop.