Four fixes from adversarial review of the branch.
ios: clamp the unread counter at the apply point. changeUnreadCounter(chatIndex:)
applied the delta unclamped, so unreadCount could go negative and the app icon badge
with it - a state that persists until the chat list reloads and that also suppresses
the badge for genuinely unread messages arriving after. Reachable because the
edit/delete sync added removeChatItem as a decrement source for member support items,
while the count being decremented excludes them (the chat list queries filter
group_scope_tag IS NULL): after a restart, moderating a still-unread support message
decrements a zero. Clamped at the collector flush - its only caller - so it is correct
regardless of how the debounced deltas batch, and the user/badge counter is fed the
delta actually applied. Android has always done this in decreaseCounterInPrimaryContext.
android, desktop: gate both primary-context mirrors on memberPending. The primary's
preview block runs for a scoped ChatInfo only when memberPending, so for anyone else
the mirrored call could not update a preview - it only had side effects. addChatItem's
mirror re-ordered the group in the main chat list when a non-pending member replied in
a member support chat, and, lacking a scope check, also re-ran the primary for sent
items the dispatcher had already given it. ComposeView's edit mirror is on the live
message path, so it fired on every tick to do nothing.
android, desktop, ios: don't let upsertChatItem create a chat entry from a scoped
ChatInfo. The memberPending exception made its "chat not found" branch reachable for
support items, seeding the main list with a support item as preview - and since
ChatInfo.Group.id ignores the scope, shadowing the real group rather than showing as a
duplicate. On iOS it also set itemAdded, which chatItemSimpleUpdate turns into a new
message notification. addChatItem never did this either.
android, desktop, ios: gate the media preview exception on showChatPreviews. Treating a
caption-less media support message as "has content" skips the status branch, but the
next branch needs showChatPreviews and Android/desktop have no trailing else (the
thumbnail is gated by the same flag) - so with previews off the row rendered empty
where master showed "reviewed by admins". iOS redacted it rather than dropping it, but
also lost the status. Falls back to hasMsgContent when previews are off.
Collapse the four nested branches in addChatItem's preview pick into a
single "keep the current preview" predicate: the new item wins by default,
and only the comparison criterion changes for a pending invitee. Same
behaviour, addresses review feedback (4 branches for 2 outcomes).
The apply-point clamp in changeUnreadCounter (342e270a1/fb849aa2c) guarded a pre-existing, general
iOS over-decrement (markChatItemsRead decrements by the requested id count regardless of whether each
item still counted) that this feature neither introduces nor worsens - Android already self-clamps in
decreaseCounterInPrimaryContext, iOS never did. The pending-invitee preview's support-item unread is
balanced without it (+1 on receive, -1 on read/delete). Leave iOS unread untouched here; a general
clamp belongs in its own change.
342e270a1 clamped removeChatItem's decrement by reading unreadCount synchronously, but
UnreadCollector debounces ~1s, so the read saw a stale value: receiving an unread item then deleting
it within the window skipped the -1 and left the badge +1 too high - a regression, since the
unconditional -1 had netted to 0 in the collector. Revert that call-site clamp and instead clamp in
changeUnreadCounter(chatIndex:) - the point the accumulated delta is applied (its only caller is the
collector flush): clamp the per-chat count at 0 and feed the badge the clamped delta. Correct
regardless of debounce ordering, and still prevents the original negative drift.
The prefer-content check used hasMsgContent (msgContent != null AND non-empty text), so a
caption-less photo/voice/file was treated like a no-content event and could not become the preview
when a prior text message was shown. Use msgContent != null (is-a-message) instead; the no-content
event re-cover guard still holds (events have msgContent == null).
removeChatItem decremented the unread counter unconditionally for an RcvNew item. iOS, unlike
Android's self-clamping decreaseCounterInPrimaryContext, could drive the badge negative if the item
entered the list via upsertChatItem's addChat path (no increment) and was then deleted unread. Only
decrement when the chat's unreadCount > 0.
The prior change to render caption-less media (instead of the status text) used the shared
ChatPreviewView condition, so it affected every chat with a status preview (direct connection
states, other group states). Gate it on memberPending so only the pending invitee's member-support
preview changes; all other chats keep the original hasMsgContent behaviour.
ChatPreviewView showed the status info text (e.g. a pending invitee's 'reviewed by admins')
whenever the last item's text was empty (hasMsgContent), which also caught caption-less media — so
a photo/voice/file support message showed the status text next to its already-rendered thumbnail.
Show the status only for true no-content items (events, msgContent == null); any message, including
caption-less media, falls through to render its content.
Editing in a member-support scope calls upsertChatItem on the active (secondary) context only, so
the primary context that owns the main-list preview was never updated (same asymmetry as sends).
Also update the primary context on edit when the active context is a member-support scope. iOS is
unaffected (single chats list).
upsertChatItem/removeChatItem gated their main-list preview update on groupChatScope()==null, so
once a pending invitee's support message was the preview, editing it left stale text and
deleting/moderating it left a phantom. Add the same memberPending exception addChatItem has (both
already match the preview item by id). On iOS, restrict the 'update preview for an item not in the
open scope' clause to main scope so a support item's status updates don't churn the preview.
A pending invitee's member-support no-content group events (SGEUserPendingReview on the
PendingApproval->PendingReview transition, member-connected/E2EE/feature items) are delivered
as new chat items in member-support scope. The pending preview took the newest item, so such
an event re-covered the first message and rendered as the static "reviewed by admins" text.
Prefer a content message: a no-content item no longer displaces an already-shown message.
Mirror the android/desktop fix: a pending invitee's received support message was
dropped from the main chat list preview by the group itemTs comparison (received
broker clock vs locally-stamped group-event clock - not comparable). Bypass the
comparison when memberPending so the received support item surfaces in the preview.
- Narrow the sent-item mirror (previous commit) to GroupChatScopeContext (member
support); the reports view has no compose, so it shouldn't trigger it.
- A pending invitee's RECEIVED support message was dropped from the main chat list
preview: the group preview keeps the higher-itemTs item, but a received message's
itemTs is the broker clock while the placeholder group event's itemTs is the local
device clock - an unreliable cross-clock comparison. Bypass it when memberPending
so received support items surface in the preview the same way sent ones do.
A message sent in a secondary scope (member support / "chat with admins") is
added only to the active context (ComposeView passes its own chatsCtx), unlike
received items which the NewChatItems handler delivers to both the primary and
secondary contexts. After the index-correct revert of #5909's chatsContext.chats[i]
write, a pending invitee's sent support message no longer appeared in the main
chat list preview.
Mirror the receive path: when adding a sent item on a secondary context, also
update the primary context, so the main chat list preview updates at the correct
index. Sent items are not RcvNew, so this adds no unread double-count.
When a member support chat or group reports view is open (a secondary chats
context is active), incoming messages could overwrite the last-message preview
of an unrelated chat in the main chat list, increment the wrong chat's unread
badge, and pop the wrong chat to the top.
ChatsContext.addChatItem computed the chat index against the receiver context's
own list via getChatIndex, but then read/wrote chatsContext.chats[i], which
always refers to the primary (main) chat list. On the secondary context the two
lists are ordered differently, so the index pointed at a different chat.
Use the receiver context's own list (chats[i]) consistently, matching every
other method in ChatsContext and the iOS implementation. No change for the
primary context, where chats and chatsContext.chats are the same list.
On an incoming call the desktop window blinked continuously (FileNotFoundException
from SoundPlayer.start writing into a missing ...\Temp\simplex).
Root cause: Files.desktop top-level val initializers run in any process that
touches the facade class, including a transient second instance (which reaches
acquireSingleInstance -> dataDir). Two of them deleted shared state used by the
running primary:
- tmpDir had .also { deleteOnExit() } - the second instance's normal exit deleted
...\Temp\simplex, so the primary then failed to write call sounds/recordings.
- preferencesTmpDir had .also { deleteRecursively() } - the second instance's
<clinit> wiped configPath\tmp (same anti-pattern, firing even earlier).
Make both declarations pure and do the destructive cleanup in Main, past the
single-instance check, so only the owning instance performs it (preferencesTmpDir
kept early, before any settings write). The remaining val-initializer side effects
are idempotent creations (mkdirs) that destroy nothing and are left in place.
Wide images size their preview box with a fixed width of DEFAULT_MAX_IMAGE_WIDTH
(500dp). #7125 switched the box from .aspectRatio() to a fixed .height() computed
from that nominal width; on screens narrower than 500dp, .width(500dp) is clamped
to the available width but the fixed height is not, so the top-aligned FillWidth
image is shorter than its box, leaving an empty strip below.
Compute the height from the width actually granted via a small Modifier.layout so
it tracks the clamped width, restoring the self-correcting behaviour .aspectRatio()
had. coerceAtMost(w) keeps the box within its nominal width (and bounds the
unbounded intrinsic-measurement pass), and coerceAtLeast(0) mirrors what
Modifier.width()'s SizeNode does for a negative w on a tiny window - so both
dimensions stay in range and the #7123 Constraints overflow crash cannot recur.
* ui: what's new in 7.0 (#7265)
* docs: add Hosting your own Chat Relay guide (#7263)
* docs: add Hosting your own Chat Relay guide
* cli: run headless relay without a terminal
--headless (no -e) now runs via simplexChatCore instead of the terminal
UI, so the relay runs as a systemd service without a TTY (withTerminal
requires one). Drains the event queue logging only errors, and sets
stdout to line-buffering so logs reach the journal.
* docs/chat-relay: debounce service
* Apply suggestions from code review
Co-authored-by: Evgeny <evgeny@poberezkin.com>
---------
Co-authored-by: Evgeny <evgeny@poberezkin.com>
* Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 47.9% (179 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/zh_Hant/
* Translated using Weblate (Turkish)
Currently translated at 63.0% (235 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/tr/
* Translated using Weblate (Indonesian)
Currently translated at 95.7% (357 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/id/
---------
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Evgeny <evgeny@poberezkin.com>
Co-authored-by: HUGOALH <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: Omer <abulomer2001@gmail.com>
Co-authored-by: Rafi <rafimuhmad90@protonmail.com>
* docs: add Hosting your own Chat Relay guide
* cli: run headless relay without a terminal
--headless (no -e) now runs via simplexChatCore instead of the terminal
UI, so the relay runs as a systemd service without a TTY (withTerminal
requires one). Drains the event queue logging only errors, and sets
stdout to line-buffering so logs reach the journal.
* docs/chat-relay: debounce service
* Apply suggestions from code review
Co-authored-by: Evgeny <evgeny@poberezkin.com>
---------
Co-authored-by: Evgeny <evgeny@poberezkin.com>
getLoadedImage subsampled with an OR cap on the larger side, so tall
images (e.g. phone screenshots) were reduced far below the display size
and rendered blurry. Mirror Android's calculateInSampleSize (keep the
smaller side at the target) so previews stay sharp, and add a decoded-
pixel ceiling so extreme aspect ratios can't blow up decode memory.
* desktop: show startup errors in a copyable window instead of bare "Failed to launch JVM"
When any exception escapes main() before the app window appears - a missing
DLL, a failed migration, broken AWT init - the jpackage launcher shows only
"Failed to launch JVM" and the cause is recorded nowhere: the launcher runs
without a console, so stderr is lost. Every report in #4146 stalled on this.
Catch the error and show it in a native Win32 window laid out like a message
box: an error icon and message, two clickable report links (the GitHub issue
tracker and the support email) above a read-only selectable box with the stack
trace, and an OK button. The links are SS_NOTIFY statics opened with
ShellExecute (browser for the URL, mail client for the email). Native, not
Swing, because broken AWT initialization is one of the failure causes. On
Windows the process then exits cleanly so the launcher does not also show its
own box; on other systems the error is rethrown to stderr.
* docs: plan justifying desktop startup error window (#4146)
* desktop: bundle jdk.accessibility to fix "Failed to launch JVM" when assistive technologies are enabled (#4146)
The jlinked runtime shipped in desktop packages did not include the
jdk.accessibility module. On Windows, when Java Access Bridge is enabled
(jabswitch -enable, "Enable Java Access Bridge" in Ease of Access, or a
screen reader creating %USERPROFILE%\.accessibility.properties), AWT
throws AWTError "Assistive Technology not found:
com.sun.java.accessibility.AccessBridge" during Toolkit init, before any
window or log output, and the jpackage launcher reports "Failed to
launch JVM".
* docs: plan justifying jdk.accessibility fix for Windows JVM launch (#4146)
* desktop: bundle jdk.accessibility only when building the Windows package