Opening a video full screen could crash with NoSuchElementException from
VLC native-library discovery. Each MediaPlayerFactory() runs a JDK
ServiceLoader (not thread-safe), and the second preview factory added in
#6924 let the render thread and preview thread construct factories
concurrently. Serialize the two constructions behind a shared lock.
* android, desktop, ios: remove left padding on consecutive received messages in channels
In channels, a received message that does not show an avatar (a consecutive post from the same sender) drops the avatar-sized left padding and sits flush-left. Applies to both owner broadcasts (ChannelRcv) and contributor posts (GroupRcv); the first message of each run still shows the avatar. Gated on ChatInfo.isChannel, so regular groups, business and direct chats, sent messages, and avatar-shown messages are unchanged.
* docs: add plan justifying removing left padding on consecutive received messages in channels
* ios: fix right gap on consecutive received messages in channels
Removing the avatar-sized left padding from no-avatar received messages
(this PR) shifted those bubbles ~44pt left, but maxWidth still reserved
the avatar inset, so consecutive messages stopped ~44pt short of the
first (avatar) message on the right.
Widen maxWidth for no-avatar channel-received items so their right edge
matches the avatar-shown first message. The no-avatar predicate reuses
the exact shouldShowAvatar expression from the render path (lifted to a
file-scope function so the maxWidth site can call it), so the width and
the rendered layout can never disagree.
Android is unaffected: Compose derives content width from padding, so
reducing the start padding already widened the row there.
* ios: increase left padding
* kotlin: increase left gap
---------
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
* android, desktop: fix crash on opening chat with extremely wide image
An image with an extreme aspect ratio (e.g. 4000x1) made the chat
unopenable: the framed item's Box clamped its aspectRatio only on the
low side (coerceAtLeast(1f / 2.33f)), leaving very wide images
unbounded. During an intrinsic measure pass Compose derives
width = height * ratio, which for a 4000:1 image overflows Constraints
and throws IllegalArgumentException on every render.
Add the symmetric upper bound (coerceIn(1f / 2.33f, 2.33f)), matching
the existing tall-image height cap in PriorityLayout
(constraints.maxWidth * 2.33f).
* docs: add plan justifying wide-image chat crash fix
* desktop: fix updater deleting the download before "Open file location"
The in-app updater downloads to a temp UUID file via createTmpFileAndDelete,
then relies on `file.renameTo(newFile)` to move the bytes to the asset name so
they survive that helper's `finally { tmpFile.delete() }`. The rename's return
value was ignored: if it failed, the bytes stayed at the UUID path and the
finally block deleted the only copy, so the "Download completed" dialog appeared
but "Open file location" opened an empty /tmp/simplex.
Use Files.move with REPLACE_EXISTING instead. It performs the same in-place
rename when possible (verified: inode preserved, no copy), falls back to
copy+delete when an atomic rename isn't possible, and throws on genuine failure
- which the existing outer catch handles - instead of silently losing the file.
* docs: plan for updater open-file-location fix
* docs: plan - note Whonix compatibility (updater previously failed there)
* android, desktop, ios: remove right gap on received messages in channels
In channels received messages now use the full row width instead of the chat-bubble right gap, matching the broadcast/feed style. Gated on ChatInfo.isChannel (useRelays), the always-present channel flag used across the channel UI; sent messages and non-channel groups, business and direct chats are unchanged.
* docs: add plan justifying removing right gap on received messages in channels
---------
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
* ios: open SimpleX links in chat messages via in-app connect flow
Tapping an inline SimpleX connection link in message text was dispatched
through UIApplication.shared.open. iOS drops an open() of a URL owned by
the same app while it is in the foreground (the simplex: scheme and the
simplex.chat universal links both belong to this app), so the tap was
ignored and never reached the connection flow. Web links (Safari) and
mailto:/tel: (other apps) were unaffected, which is why only SimpleX
links appeared dead.
Route SimpleX links to ChatModel.appOpenUrl instead - the same sink
onOpenURL feeds, leading to connectViaUrl/planAndConnect. This matches
the connection-link card and the multiplatform clients, which connect
in-process rather than via an OS round-trip.
Also fixes the same problem for the "Send questions and ideas" and
"connect to SimpleX Chat developers" buttons, which open simplexTeamURL
(a simplex: link) the same broken way.
* docs: plan - justify iOS in-app dispatch for SimpleX links in messages
Root cause and justification for opening inline SimpleX links via the
in-app connect flow instead of UIApplication.shared.open (undefined
re-entry of the same foreground app for a self-owned simplex: URL).
The API.Error branch in apiUpdateGroup rendered "$r.err", printing the API.Error object reference plus a literal ".err" instead of the error message. Use "${r.err.string}" so the actual error is shown.
A long file name took all the row width and squeezed the cancel (X)
icon to zero, so the file could not be dismissed before sending.
Give the file-name text the layout weight and a single line (Compose),
and lineLimit(1) on iOS, so it truncates and the close icon keeps its
space. Affects Android, Desktop and iOS.
In groups and channels with thousands of members, opening any screen that
shows the member list could briefly freeze the app. The most noticeable case
was the "Chats with members" screen: closing a member's chat and returning to
the list reloaded everyone and stuttered each time.
The app was re-checking every member against every other member while loading
the list - work that grows with the square of the group size, so it got
dramatically slower as groups grew. It now does this in a single pass, so
member lists, @-mentions, channel relays and adding members all stay
responsive even in very large groups. You see exactly the same members, in
the same order - just without the lag.
* android, desktop: skip media with no decodable preview to fix IndexOutOfBounds on send
processPickedMedia appended to MediaPreview.content unconditionally for videos
(and size-ok animated images) but only appended to images when a preview bitmap
existed. getBitmapFromVideo returns a null preview for undecodable/corrupted
videos without throwing, desyncing the two lists; sendMessageAsync then indexes
images[index] past its end and crashes. Pair both appends behind a non-null
bitmap so the lists stay equal-length and index-aligned, and skip only the bad
item so the rest of the picked batch still sends.
A video skipped this way shows showVideoDecodingException(), guarded by
hasAlertsShown() so it neither stacks across multiple bad items nor duplicates
the alert already shown on getBitmapFromVideo's exception path. Image decode
failures are already surfaced earlier by getBitmapFromUri.
* docs: add plan justifying media preview alignment fix
* cli: remove help entries for commands removed long ago
/pq and /pq @<name> were removed in #4049 (PQ encryption for contacts
is now automatic); /get stats and /reset stats were removed in #4375
(legacy agent stats). All four were left documented in CLI help, so
typing them fails. Remove the stale entries.
* plans: justify removal of outdated CLI help entries
* plans: drop //get stats mention from CLI help plan
* android, desktop, ios: show chat name in delete / leave / clear confirmation dialogs
The dialogs confirming delete contact, delete/leave group/channel and
clear chat now show the chat's name on its own line above the existing
warning, so the user can see which chat the destructive action will
affect.
Pure code change: no new translation strings, no signature changes, no
new helpers. The name is read via existing displayName accessors on
GroupInfo / Contact / ChatInfo.
clearNoteFolderDialog is intentionally unchanged - the notes folder is a
single-instance object and its existing warning already identifies it.
* android, desktop, ios: also show chat name when deleting pending connection
deleteContactConnectionAlert was missed in the original inventory.
Same pattern as the other dispatchers - prepend the connection's
displayName on its own line above the existing warning - so a user
who set a custom name on a pending connection can see which one
they are about to delete.
* android: use <br> instead of \n for newline in delete confirmation dialog body
On Android, the alert body goes through HtmlCompat.fromHtml which
treats the input as HTML and collapses literal \n to a single space -
so "Tech Talk\n\nGroup will be deleted..." rendered as
"Tech Talk Group will be deleted...". Switch to <br><br>, which both
HtmlCompat (Android) and the Desktop parser at Utils.desktop.kt:75
correctly render as a newline.
* android, desktop: skip HTML parsing for delete confirmation dialog text
Add parseHtml: Boolean = true parameter to showAlertDialog and
showAlertDialogButtonsColumn; when false, the body text is wrapped as
AnnotatedString and routed through the existing AnnotatedString
AlertContent overload, bypassing escapedHtmlToAnnotatedString entirely.
The 8 dispatchers that embed the user-controlled chat displayName now
opt out (parseHtml = false). This means:
- displayName is rendered as literal text - no HTML interpretation,
so a contact whose alias is "<b>X</b>" or "<font color=..." no
longer renders bold or coloured in the confirmation dialog
- the platforms align: both iOS and Kotlin now use plain "\n\n" for
the separator (no more <br><br> Kotlin-only workaround)
- removes coupling to escapedHtmlToAnnotatedString in this path
* android, desktop: shorten parseHtml comment in AlertManager
* remove extra text
---------
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
onFilesAttached classified URIs by isImage only, so videos fell through
to processPickedFile and were attached as generic files. processPickedMedia
already handles video correctly; the classifier above it just never
reached that branch. Recognise videos as media inline using getFileName.
* android, desktop: fix chat item long-press menu and ripple shape
clipChatItem clipped the bubble shape with Modifier.clip. Modifier.clip
of the bubble GenericShape mis-hit-tests its path on very tall items, so
long-press on the lower part of a long message did not reach
combinedClickable and the context menu did not open (#6991); on desktop
the same clip also left the press ripple rendered as a rectangle.
Clip the bubble GenericShape in the draw pass (drawWithCache + clipPath)
instead: drawing is clipped identically, the press ripple included, with
no effect on hit-test. The RoundRect shape (tail disabled) hit-tests
correctly and keeps Modifier.clip.
Fixes#6991
* plans: justify chat item long-press and ripple shape fix
---------
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
* plan: web previews for channels
* types for recipient side to support channel web previews and domain names
* fix
* migrations
* update schema and api types
* update schema
* rename migrations
* core: check member role
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* desktop: don't copy non-message items when selecting message text
Selecting text across messages also copied the text of event/info
items (e.g. "connected") that fell inside the selection, even though
those items are never highlighted as selected.
getSelectedCopiedText emitted text for every merged item between the
selection bounds. Event/info items have no msgContent but a non-empty
text, so as interior items their text was copied. Skip items whose
content has no msgContent - only real messages are copyable.
* plans: add 2026-05-20-fix-copy-non-msg-items.md
* core: fix /start remote host parser when iface name contains a space
The iface= field used jsonP (which calls takeByteString and strict-decodes
the entire remaining input as JSON). When port= followed iface=, the strict
decode failed on the trailing data and the text1P fallback stopped at the
first space inside the JSON-quoted interface name (e.g. "Ethernet 2"),
leaving unparseable junk and producing "Failed reading: empty".
Replace jsonP with a bounded quotedP that consumes only up to the closing
quote, leaving port=… for the next parser.
* plan: document fix for /start remote host iface-with-space parser bug
Move SectionDividerSpaced() out of the SectionView { ... } block so it acts
as inter-section spacing instead of being rendered as the section's last
child. Matches the pattern used by every other section in ChatInfoLayout.
Plan: plans/2026-05-25-fix-e2e-encryption-section-divider.md
* desktop: fix video playback hang caused by stuck preview snapshot
Problem: clicking play on a video did nothing when an earlier video's
preview generation was stuck — every subsequent VideoPlayer.play() was
queued behind it on the shared playerThread.
Cause: helper player reuse across previews exhausted the libavcodec h264
frame-buffer pool with --avcodec-hw=none (PR #6924), and the synchronous
libvlc snapshots().get() call then hung waiting for a frame that was
never decoded.
Fix: drop the helper-player pool (release each helper after use) and run
preview generation on a dedicated previewThread so a stuck preview can
no longer block playback.
* plans: add 2026-05-15-fix-video-preview-snapshot-hang.md
* desktop: capture preview via callback surface, keep helper pool
Follows up on the previous commit (4a964c66). The actual hang was in
libvlc's synchronous snapshots().get() on a reused helper, not in the
pooling itself. Replace the polling loop with a CallbackVideoSurface
(the existing SkiaBitmapVideoSurface) wrapped in withTimeoutOrNull —
the wait is bounded, so a non-decoding helper can't block previewThread.
Restore the helper-player pool that the previous commit dropped.
* plans: update 2026-05-15-fix-video-preview-snapshot-hang.md for final fix
* desktop: fix in-app updater silently failing on Windows
chooseGitHubReleaseAssets ran `which dpkg` unconditionally to probe for
Debian-derivative systems. On Windows there is no which.exe, so
Runtime.exec threw IOException, which the outer catch in checkForUpdate
logged and swallowed -- the update dialog never appeared. Gate the
probe on desktopPlatform.isLinux().
* desktop: fix in-app updater install step on AppImage
xdg-open on the downloaded .AppImage opened it in whatever the desktop
environment's default handler for the AppImage MIME type is -- usually
an archive viewer, which reports 'Archive format not recognized'. The
running AppImage was never replaced.
Detect $APPIMAGE (set by the AppImage runtime to the path of the
running .AppImage file). Copy the downloaded file to a staging file in
the target's own directory, mark it executable, then atomic-move it
onto $APPIMAGE. Staging in the target directory keeps the final move a
same-filesystem rename(2), so an interrupted copy never leaves the
running AppImage partially overwritten. On failure (permission denied,
target read-only, etc.) fall back to opening the parent directory so
the user can install manually -- the same fallback the existing
xdg-open path already used.
* desktop: fix in-app updater silently failing on aarch64 AppImage
The LINUX_AARCH64 githubAssetName had a literal leading space
(" simplex-desktop-aarch64.AppImage"), so the exact-name filter in
chooseGitHubReleaseAssets never matched the real release asset name
"simplex-desktop-aarch64.AppImage". The asset list came back empty
and checkForUpdate's early-return at "No assets to download for
current system" suppressed the dialog. Same silent-failure pattern as
the Windows bug.
* plans: justify desktop in-app updater fixes
* core: update simplexmq
* update agent api
* update simplexmq
* core: add flag to User to use client services
* update simplexmq
* cli command to toggle service for a user
* test, fix
* query plans, core/bot api types
* remove local package reference
* increase server queue size in tests
* show client service status in users list
* update query plans
* cli: fix redraw slowness (#6735)
* cli: add pland to fix redraw slowness
* updtae doc
* cli: decouple key reading from processing via TQueue
* schema and bot types
---------
Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>
* desktop: prevent duplicate launches
Acquires a file lock and listens on a loopback ServerSocket in dataDir.
A second launch signals the running instance to restore its window and
exits silently. See plans/2026-05-13-desktop-single-instance.md.
* desktop: un-minimize window in showWindow
toFront() does not un-minimize a JFrame on any AWT platform. Clear the
ICONIFIED bit so a minimized window restores; preserves MAXIMIZED_BOTH.
Also fixes the same case when restoring from the tray icon.
* desktop: move showWindow from DesktopTray to DesktopApp
It has callers outside the tray (single-instance signal) and belongs
next to simplexWindowState, which it operates on.
* simplify
* refactor
* desktop: start show-file watcher when choosing minimize from first-close dialog
The handleCloseRequest path already starts the watcher when minimizing to
tray; the Ask-dialog path did not, so the first-time user who picks
"Minimize to tray" got a hidden window with no signal handling — a
duplicate launch would not restore it.
* desktop: always watch for duplicate-launch signal, drop hung-instance alert
The watcher now runs for the JVM lifetime once the lock is acquired,
not only when minimized to tray. Duplicate launches always restore the
primary's window (un-minimize, un-tray-hide, toFront) instead of being
silently dropped when the primary is not minimized.
Drops the "may be hung, start anyway?" popup and the two strings — that
fallback was needed only because the watcher could miss signals. With
the always-on watcher there is no scenario where the primary fails to
consume simplex.show, so the escape hatch becomes dead code.
* desktop: alert when primary's watcher doesn't consume the show file
Restores the "another instance may be running" alert. Every duplicate
launch waits up to 1s for the primary's watcher to delete the show file
it just created. If the file is consumed within the window, the
duplicate exits silently. If still there after 1s the primary is hung
and the alert fires.
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* core, ui: persist "Remove link tracking" setting on database import
The privacySanitizeLinks preference was stored locally only and absent from
the AppSettings round-trip, so it was lost when migrating to another device
or after a fresh install + DB import. Add the field to the Haskell, Kotlin,
and Swift AppSettings payloads and wire it through iOS group defaults.
* plans: justify privacySanitizeLinks AppSettings round-trip fix
Companion to #6958 (Android/Desktop). Channel-link "Share via chat"
picker showed Saved Messages, which produced `Failed reading: empty`
on tap because `*<id>` has no branch in `sendRefP`. Add `includeLocal`
flag to `filterChatsToForwardTo` (default true) and to
`ChatItemForwardingView`; `shareChannelPicker` passes false.
Bug #2 from #6958 (button on plain groups) is not present on iOS —
`GroupLinkView.swift:110` already gates by `publicGroup != nil`.