Commit Graph
138 Commits
Author SHA1 Message Date
spaced4ndy 7af42558c5 Merge branch 'master' into f/directory-integration-plan 2026-08-27 13:11:04 +04:00
EvgenyandEvgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> 17cdef1692 core: include channel link and name when forwarding messages (#7409)
* core: include channel link and name when forwarding messages

* wip

* simplify

* add member ID

* refactor

* refactor

* refactor

* update api types

* store forward source group type

* rename

* api types

* simpler layout

* layout, translations

* refactor ios

* public

* simpler

* refactor kotlin

* padding

* padding

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-08-24 21:36:57 +01:00
Narasimha-scandsh 1196d362ee desktop: animate GIFs and animated WebP (#7365)
* desktop: add bounded animated image decoder

Skia's Codec is already on the desktop classpath through skiko and decodes
both GIF and animated WebP. The frames come from a file somebody else
composed, so the decoder is bounded before it allocates: the raster is
measured in bytes with the sides multiplied as Long, each side is capped
separately so an extreme aspect ratio cannot slip under the byte budget, and
the encoded size is checked before the bytes are copied into native memory.
Anything outside the bounds, or any failure, keeps the still image the chat
already renders.

Nothing calls this yet.

* desktop: animate GIFs in chat items and full screen

Both views drew the first frame only. The full screen view also decoded its
still on every recomposition, which an animation recomposes once per frame,
so that decode is remembered against the data it comes from.

The chat list preview stays a still image: it is a 36dp box that the desktop
layout keeps on screen the whole time, so animating it would hold a raster and
spend a frame of work per listed chat, without pause.

Removes the two markers left for this work.

* desktop: don't decode animation frames that cannot be seen

With media blur on, a blurred image is only revealed while the mouse is over
it, so every frame was decoded, uploaded and then blurred away again for
nobody - and the blur is a render effect re-run per frame. Frames now decode
only while the image can be seen, which also stops motion showing through a
blur that is there to hide it.

Passing the blur state to the view is why the shared signature changes; coil
drives its own animation on Android, so there is nothing to pause there.

* docs: move animated images plan to plans/

* docs: drop file path references from animated images plan

* docs: correct animated images plan against the code

* desktop: correct animated image comments

* desktop: reduce animated image comments

* desktop: correct and bound animated image decoding

* docs: correct animated images plan against measurements

* desktop: fuse the animation prior frame decision

* docs: cover desktop animated images in spec and product

* desktop: drop the unused animated image component

* desktop: return the animation frame instead of its state

* docs: correct the animated images documentation

* desktop: don't decode animations under the full screen viewer

* desktop: bound the frames an animation rebuilds

* desktop: pause animations under any full screen modal

* desktop: stop animations that alternate expensive frames

* desktop: read what playing a frame needs only once

* desktop: close the codec of an animation outside the bounds

* desktop: wait out what an animation frame cost to decode

* docs: correct animated images claims against the code

* desktop: bound the frame count where the others are bounded

* desktop: don't wait out a stall an animation frame did not spend

* desktop: say what the slow frame constants stand for

* desktop: don't decode animations behind a minimised window

* desktop: make the animation frame wait testable

* desktop: bound the file size where the others are bounded

* desktop: pin the frame wait clamp in its test

* desktop: keep the frame wait clamp private

* desktop: reduce animated image comments

---------

Co-authored-by: sh <github.shum@liber.li>
2026-08-21 20:30:11 +01:00
Narasimha-sc 11c7a62a38 desktop: fix stretched video preview and playback for AV1 videos (#7391)
* desktop: fix stretched video preview and playback for AV1 videos

libvlc passes the padded size the decoder allocated to the buffer format
callback, not the size of the picture. dav1d pads to a multiple of 128, so
a 1920x1080 AV1 video arrives as 1920x1152, and vlc scales the picture to
fill it - the preview sent with the message, and desktop playback, were
6.7% too tall. H264 pads much less, so it was barely visible there.

Ask for the size of the track being played instead. It is already populated
when the buffer format is negotiated, and matching the track that is playing
matters for files with more than one video track, where the first track is
not necessarily the one being decoded. Falls back to the previous behaviour
when the track is not known.

* plans: desktop video preview aspect ratio
2026-08-19 14:34:07 +01:00
Narasimha-sc 222fc4ad99 android, desktop: open group member profile without loading all members (#7388)
* android, desktop: open group member profile without loading all members

Clicking member avatar in chat loaded the whole member list (apiListMembers)
before showing member profile, and it was repeated on every click - in a group
with 10000 members it takes several seconds.

The full list is not needed to show the profile of one member, so instead the
opened member is added to the model, the same way as in iOS app.

* plans: member profile in large groups

* plans: correct relay warning section - it is not affected by the change
2026-08-19 14:33:18 +01:00
ecb008b792 core, ui: auto-accept group invitations per user profile (#7377)
* core, ui: auto-accept group invitations per user profile

Add a per-profile toggle for auto-accepting group invitations, and regroup it
with the existing contact-requests setting under a single Auto-accept section
in Privacy & Security, relabelled "Contact requests in groups".

The join is fully async. processGroupInvitation already had an async accept
path, used when the invitation matches a group link the user opened:
prepareAgentJoin + createMemberConnectionAsync + joinAgentConnectionAsync,
with the outcome reported later against the CFJoinConn command id. Auto-accept
takes that same path instead of going through APIJoinGroup, so it works while
the app is closed and never blocks message processing.

An auto-accepted invitation still records a CIRcvGroupInvitation item in the
chat with the inviting contact, so there is a record of who added the user to
which group.

Two details worth noting for review:

hostContact is reported to clients only for group links. Clients respond to it
by replacing the transient host connection view with the group and removing
that chat - correct for a group link, where the contact is a placeholder, but
wrong for a plain invitation, where it is a real contact.

A resent invitation returns the existing group, because createGroupInvitation
is idempotent on inv_queue_info. The join therefore only runs while the
membership is still GSMemInvited, so a resend cannot open a second connection.

* booldef

* order

* refactor

* update translation key

* query plans

* ios: export translations

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2026-08-19 00:10:54 +01:00
Evgeny Poberezkin 1160215570 Merge stable 2026-08-18 17:16:21 +01:00
EvgenyandEvgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> 5e45fe1f0e directory: only create group links after approval (#7356)
* directory: only create group links after approval

* update test

* update messages

* diff

* get group and link in one query

* reduce database reads

* better errors

* typos

* query plans

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-08-18 16:35:37 +01:00
spaced4ndy 9170a3041f wip 2026-08-13 21:07:22 +04:00
spaced4ndy 261e6103f8 wip 2026-08-12 18:31:55 +04:00
Evgeny Poberezkin 0f45645afe Merge branch 'stable' 2026-08-12 15:30:16 +01:00
Narasimha-sc abd5954675 ui: send dropped .webm as video only when it has a video track (#7354)
* ui: send dropped .webm as video only when it has a video track

.webm is as commonly an audio-only container as a video one, so the
extension alone cannot tell whether there is a frame to embed. Read the
container to decide: files with a video track are sent as video, the rest
as files.

Only done for files attached without the user saying how to send them
(drag & drop, paste). An explicitly picked video is still sent as one, so
.webm is now listed in the video file picker too.

* plan: send dropped .webm as video only when it has a video track
2026-08-12 15:29:43 +01:00
spaced4ndy 247e327ebe wip 2026-08-11 19:55:39 +04:00
spaced4ndy b999b585cf update plan 2026-08-11 12:10:30 +04:00
spaced4ndy 687f437d83 wip 2026-08-10 19:08:48 +04:00
spaced4ndy 105c18f2f2 Merge branch 'master' into f/directory-integration-plan 2026-08-10 17:45:36 +04:00
Evgeny Poberezkin b832d7c8f7 Merge branch 'stable' 2026-08-07 10:14:02 +01:00
Narasimha-sc f921bd47bb android, desktop: fix live message sent to the chat opened after switching (#7323)
* android, desktop: fix live message sent to the chat opened after switching

A live message is sent when the chat is switched, but by then this view
already shows the chat that was opened - the effect that sends it runs
with that chat, so the message typed in one chat was sent to another.

The chat the message was composed in is passed to the send, and it is
resolved by the chat id from before the switch. If that chat is no longer
there the message is not sent at all, rather than sent to the chat opened
instead. The draft cleared after sending is the one of that chat too.

* plan: correct references; clear the draft of the chat the message is sent to

* plan: note the blast radius and how to resolve the overlap with #7308

* android, desktop: only pass the chat to what a live message can reach

A live message has no context item, so the forwarding, editing and
reporting branches of the send cannot run for it - they keep using the
chat of the view, and the chat it was composed in is passed only to the
message send, to the update of an already sent live message, and to
clearing the draft after sending.

* android, desktop: give the opened chat its own compose state while the live message is sent

Sending the live message to the chat it was composed in is not enough on
its own: composeState is shared between the chats opened in this view, and
the chat switch branch of KeyChangeEffect is the only one that neither
resets it nor loads the opened chat's draft - the branch that loads a
draft is later in the same if chain and cannot be reached.

sendMessageAsync then made it visible. It runs on Dispatchers.Default, so
its writes land after the switch: the whole composed state (via
cs.copy(liveMessage = null)) and its spinner (sending()) were written to
the compose state of a view that already shows another chat, which then
displayed the text composed in the previous one until the send completed.
The draft it should have shown was still in the model, and the next switch
away dropped it.

- sendMessageAsync takes composed, and sendMessage takes it as null by
  default, so only the chat switch passes a state and every other sender
  reads it inside the coroutine, where the send read it before. The chat
  switch captures it on the main thread before replacing it - otherwise
  the send would read the compose state of the chat that was opened and
  send its draft to the previous chat.
- checkLinkPreview takes that state too. It re-read composeState rather
  than what was passed in, and every text live message reaches it through
  updateMsgContent, so it would have rebuilt the message from the opened
  chat's draft instead of committing what was composed.
- every composeState write in sendMessageAsync is guarded by
  composeIsForSend() (toChat.id == chat.id), which compares the two chats
  instead of checking which one is open, so this send never takes the
  compose state back if that chat is opened again before it completes.
- the chat switch branch then resets composeState to the opened chat's
  draft, or to an empty state, like the branches below it do.

* plan: document the compose state handoff; correct the #7308 overlap resolution

The note on resolving the overlap with #7308 said its cs.liveMessage !=
null clause "already covers the send made by the chat switch". It does
not - in #7308 that clause sits outside the chatIsOpen check, which is
correct only while a live message is always sent to the chat that is
open, the assumption this fix removes. Read as written it exempts the
chat switch send from the guard that protects the opened chat, and a
merge that follows it reintroduces the leak.

Also records what manual test 2 was found failing on, and adds a slow
send variant so the window between the switch and the send completing is
long enough to type in the chat that was opened.

* android, desktop: keep reading the current state where a forward appends it

checkLinkPreview taking the composed state is needed where a live message
reaches it, but the forwarding branch is not one of those - it cannot run
with a chat other than the view's - and forwardItem suspends before it. So
there the captured state is stale by a network round trip, and text typed
while the forward was in flight stopped being appended to the message it
adds, while still being cleared when the send completed.

* plan: correct references and the claims that no longer hold

Line references were against the base this branch forked from, before
#7308 landed. Also: the live message loop no longer exits because the send
clears liveMessage - the chat switch replaces the compose state, on the
main thread, before the send runs; checkLinkPreview is not passed the
captured state everywhere; and chatsCtx.getChat can only return null in a
secondary context, which is not how "the chat is gone" reads.

Adds the two manual checks the review implied: a live message carrying a
link preview, which is what breaks if checkLinkPreview stops reading the
state it was given, and returning to the chat before the send completes.

* android, desktop: narrow the change to what the fix needs

- the draft id a failed message is saved under keeps using chat: that
  branch is behind !liveSend, which the send made by a chat switch never
  satisfies, so toChat is always chat where it is read;
- the state the chat switch installs no longer carries maxFileSize over.
  That field is kept in sync on chat switch by LaunchedEffect(chat.chatInfo),
  which is why the branches below this one construct it without one, and
  the paths where that effect does not re-run are the ones where the send
  overwrites the state anyway;
- sendMessageAsync takes its cs as a parameter rather than aliasing a
  separately named one.

* plan: follow the narrowed change

* android, desktop: shorten the comments

The threading mechanism behind cs is explained where it is used, so the
function comment only has to say what it is; the rest is rewording.
2026-08-06 23:08:15 +01:00
spaced4ndy 33af132085 wip 2026-08-05 21:55:54 +04:00
spaced4ndy e54aaf71e4 wip 2026-08-05 21:55:05 +04:00
spaced4ndy 64ef22755d wip 2026-08-05 21:40:14 +04:00
spaced4ndy 660084f24b wip 2026-08-05 21:25:14 +04:00
spaced4ndy 844e340509 wip 2026-08-05 21:20:20 +04:00
spaced4ndy 592627e448 wip 2026-08-05 21:10:54 +04:00
spaced4ndy c875c435ac wip 2026-08-05 20:49:27 +04:00
spaced4ndy 2aa7ff3104 wip 2026-08-05 19:19:05 +04:00
spaced4ndy b9cc71d531 docs: directory integration plan 2026-08-05 16:32:38 +04:00
Evgeny Poberezkin 1a56b7f0f8 Merge branch 'stable' 2026-08-05 11:51:17 +01:00
Narasimha-sc 7e99e68950 android, desktop: fix draft appearing in another chat when switching chats while sending (#7308)
* ios, android, desktop: fix message being sent leaking into another chat

Compose state is shared between the chats opened in the same view, and
the send is launched in a scope that outlives the chat, so a send that
was still in flight when the chat was switched put its message (with the
reply context) into the compose state and then the draft of another chat,
and a late success cleared whatever was typed in the meantime.

The message being sent is no longer kept in the compose state when
leaving the chat, and the compose state is only cleared or restored after
sending if it still holds the message that was sent - the same check is
used by the other senders that show progress in the compose. A message
that failed to send is restored in the chat it was composed in, or kept
as its draft when another chat is open (iOS has no failed message
restore, there the message is dropped as before).

* android, desktop: keep only the chat switch fix

Revert the iOS changes and the same check in the three senders that
connect a prepared chat, leaving the fix for the compose state shared
between the chats opened in one view.

* plan: document what the narrowed change leaves to the connect senders

* android, desktop: use the same check where the sending flag is shared

The senders that connect a prepared chat set the same inProgress flag,
so a connect completing after the chat was switched cleared it for a
send started in the chat opened next, and that sent message was then
left in the compose.

* android, desktop: keep the live message clauses inside the open chat check

live and cs.liveMessage != null were alternatives to chatIsOpen, which
holds only while a live message is always sent to the chat that is open.
#7323 removes that: the live message committed by a chat switch is sent
to the chat it was composed in, while this view already shows another
one, so an unguarded clause here clears that chat's compose state - the
leak this fix exists to prevent.

liveSend stays an alternative to inProgress inside the guard, so a live
send behaves exactly as before while its chat is open, and it is excluded
from the restore/draft branch, which would otherwise write a draft on
every failing keystroke send once that chat is no longer open. On this
branch the only behaviour change is a live send completing after its chat
was left, which now leaves the opened chat alone.

Also load the draft of the chat opened next when the compose state is
cleared on switching away from a send in flight: clearState() returns
before the branch that loads a draft, so that draft was never shown, and
being in the slot but in no compose state it was then dropped by
clearPrevDraft on the next chat switch.

* plan: explain why live sends are guarded by the open chat check

Records that the exemption in "Deliberately unchanged" is from a guard
based on inProgress, not from chatIsOpen, and why the earlier form broke
once #7323 sends a live message to a chat other than the one open.
2026-08-04 21:01:35 +01:00
Narasimha-sc 0324517bfe android, desktop: keep database passphrase toggle inside the section card (#7326)
The "Save passphrase in settings/Keychain" label had no weight, so Row
measured it at full available width, the weighted spacer collapsed to 0
and the trailing DefaultSwitch was placed past the row's right edge.

Harmless until #6777: the section card is now inset by CARD_PADDING and
clipped with SectionCardShape, which cut the row from 348dp to 316dp on
the desktop start pane and made the overflowing switch both invisible
and unclickable, since Modifier.clip clips pointer input too. With a
custom passphrase set the toggle is enabled but unreachable, so the
passphrase stays saved in settings and the app never prompts for it on
start.

Give the label weight(1f) and drop the weighted spacer, matching what
SettingsActionItemWithContent does for every other toggle row.
2026-08-04 08:09:47 +01:00
EvgenyandEvgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> e1a349b90f core: support contact addresses with DR keys, service requests (#7310)
* core: use double ratchet keys in contact address (#7278)

* core: use double ratchet keys in contact address

* use PQ from the first message

* query plans

* update simplexmq

* api to rotate keys, option to show full links in CLI

* shorter description

* ui: add error parameters

* disable DR in addresses

* core: parameter for create address command to configure ratchet keys

* add pqRatchet param to address-related commands

* query plan

* fix parser

* fix kotlin

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* core: contact request rejection and service requests (#7292)

* update simplexmq

* implement service requests and rejections

* tests

* migration

* fix migration

* add api event and response

* bot api, postgres migration

* nix shas

* bot types, rename property

* update bot type

* sign service requests

* update bots api

* query plan

* update plan

* update simplexmq

* fix test, update bot api

* fix bot api

* resolve name for service request

* refactor

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* update simplexmq

* update simplexmq

* test delays

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-08-01 12:51:03 +01:00
Narasimha-sc 02178fb611 android, desktop: fix accepting contact request from notification for non-active profile (#7316)
* android, desktop: switch to the request's profile when accepting contact request from notification

* plans: justify accepting contact request from notification fix
2026-07-27 20:25:56 +01:00
Narasimha-sc 1108e87ee2 desktop: fix forwarding moving message draft to another chat (#7307)
* desktop: fix forwarding moving message draft to another chat

The same ComposeView is reused when switching chats, so the `chat`
captured by the desktop-only `onDispose` is the chat that was open when
the view was first composed, not the one the message is forwarded from.
The draft was saved under that stale chat id, and when it happened to be
the chat forwarded to, sending the forward restored the draft there.

* plan: justify desktop forward/draft fix and cross-platform findings
2026-07-25 18:23:04 +01:00
Narasimha-sc e7c94472c4 desktop: fix chat switch deleting the in-app updater's download (#7295)
* desktop: fix chat switch deleting the in-app updater's download

createTmpFileAndDelete registered every temp file it created in
ChatModel.filesToDelete, which ComposeView.deleteUnusedFiles() empties on every
chat open/switch/close. The updater's ~350 MB download lives in such a file for
minutes, so switching chats during it deleted the file. copyTo kept writing
through the still-open fd, so the transfer completed and progress reached 100%;
only the following Files.move failed, with NoSuchFileException on the source,
and that went into a catch that logs to stdout only - no dialog, no error, no
file.

The registration was redundant for every caller: the helper's own
finally { tmpFile.delete() } already removes the file, deleteOnExit() covers a
clean JVM exit, and Main.kt wipes tmpDir at startup for crashes. It only ever
duplicated the finally while arming a trap for long-running lambdas - the same
window also existed for the settings and themes writers.

* desktop: download the app update into a file the updater owns

Per review of #7295: instead of removing the ChatModel.filesToDelete
registration from createTmpFileAndDelete, stop using that helper for the
downloaded file.

downloadAsset now writes into "<asset name>.part" in tmpDir and moves it onto
the asset name when the transfer completes, so the download is never registered
in ChatModel.filesToDelete and ComposeView.deleteUnusedFiles() can no longer
delete it mid-transfer. Cleanup is unchanged in substance: the finally block
removes the partial file when the download fails or is cancelled (and is a
no-op after a successful move, as the helper's own finally was), deleteOnExit
covers a clean exit, and Main.kt wipes tmpDir at startup after a crash.

createTmpFileAndDelete is restored to its previous behaviour, with a comment
warning that its file does not survive a chat switch.

* desktop: reduce comments in updater download fix
2026-07-24 21:07:56 +01:00
Narasimha-sc dc3c106bda core: remove SimpleX Status preset contact (#7231)
* core: remove SimpleX Status preset contact

Preset contact cards are only created at user record creation
(createPresetContactCards), so this affects new profiles only;
existing profiles keep their stored SimpleX Status contact.

Removing the card shifts contact ids allocated after /create user
down by one, hence the test id updates.

* plans: justify SimpleX Status preset contact removal
2026-07-24 09:05:12 +01:00
Narasimha-sc 177a591466 core: fix member support chats staying marked unread after they're read (#7281)
* core: don't mark member support chat items read when reading group without scope

Reading a group without a scope marked support-scope items read without
decrementing the per-member support_chat_items_* counters, so members stayed
unread in the support list even after their chat was fully read. Restrict the
no-scope group read and its timed-items query to main-scope items.

* plans: support chat unread on no-scope group read

* core: update query plans for group scope read

The main-scope read and timed-items queries now filter on group_scope_tag
and group_scope_group_member_id, so they seek via idx_chat_items_group_scope_stats_all
(5-column) instead of idx_chat_items_groups_user_mention (3-column).

* plans: document query-plan and benchmark performance results

* tests: fix unreliable support item id capture in no-scope group read test

lastItemId returns the latest item by item_ts, which right after createGroup2
can be the group "connected" event rather than the just-sent support message.
The per-item read then targeted the wrong (already-read, main-scope) item and
never decremented the support counters, so the test failed regardless of the
fix (consistently in CI, flakily locally depending on item ordering).

Capture the support item id directly from the member-support scope instead,
keeping the change contained to this test. Verified: the test passes with the
fix and fails when the fix is reverted.

* tests: fix name-shadowing build error in no-scope group read test

The local pattern binding `itemId` shadowed the `itemId` helper imported
from ChatTests.Utils, which -Wname-shadowing (Werror) rejects. Rename the
local binding to `iId`.
2026-07-23 10:17:27 +01:00
Narasimha-sc 8de39c25de android, desktop: fix video preview expanding into empty black area (#7286)
* android, desktop: fix video preview expanding into empty black area

The video message box (CHAT_IMAGE_LAYOUT_ID) never gave itself a definite
height - it took the height of its tallest child. That child is the player
surface, which has no stable intrinsic height until playback starts: on
Android an unprepared StyledPlayerView reports no video size, so its
AspectRatioFrameLayout (RESIZE_MODE_FIXED_WIDTH, aspect 0) does not shrink.

Before #6726 this was masked: the box was measured with unbounded height, so
the surface collapsed to 0. #6726 bounded the box to 2.33x width to constrain
tall images, and the unprepared surface then expanded to fill that height,
showing as a long empty black strip below the preview once a video downloads.

Give the box a definite size from the preview aspect ratio, mirroring
CIImageView. Height follows the measured (clamped) width via Modifier.layout
so wide videos on narrow screens don't leave an empty strip (the #7223 fix).

* plans: justify video preview black-area fix
2026-07-22 13:11:54 +01:00
Narasimha-sc ab691a4f82 android, desktop: remove stray card dividers in server info, connect-to-desktop, appearance and migration screens (#7118)
* android, desktop: remove stray card dividers in server info, connect-to-desktop, appearance and migration screens

PR #6777 turned these into card screens, where SectionView draws a 2dp near-black divider between every direct child, but left content patterns that pre-date the card chrome: nested SectionViews (SMP/XFTP server detail), loose Text + Spacer + Text (connect-to-desktop device name and version), and a SectionDividerSpaced() as a middle child (Appearance with an image wallpaper, migrate-from-device "error stopping chat"). Each draws stray lines inside the card, most visible in dark/black themes.

Restructure to the sibling pattern already used by DetailedSMPStatsLayout: un-nest the server-detail sub-sections (address its own card, stats/subs/sessions as siblings; subscription rows carded), wrap each device name/version block in a single Column, and drop the in-card spacers (the adjacent-row auto-divider already separates them). Layout-only; iOS is a separate codebase and is unaffected.

* docs: add plan justifying removing stray card section dividers
2026-07-22 13:11:06 +01:00
Narasimha-sc 5a380a751f desktop: fix hand cursor over clickable commands; fix command clicks lost on quick successive clicks (#7249)
* desktop: fix pointer cursor not changing to hand over clickable commands (dropped hover events)

* desktop: set hover cursor directly on every hover move; compose pointerHoverIcon is edge-triggered and loses updates when chat items shift under the cursor

* plans: investigation and justification for command hover cursor fix

* desktop: harden hover cursor fix after adversarial review: refresh on release, ignore button-held events, reset icon state on exit, cache canvas lookup failures

* desktop: document two-stage verification, exit icon asymmetry and cache guard rationale in hover cursor fix

* plans: reconcile performance and testing claims with hardened code

* plans: correct recomposition count and note commit subject scoping

* plans: clarify user-facing problem description

* desktop: shorten comments in hover cursor fix

* desktop: clarify comments in hover cursor fix

* multiplatform: fix command clicks lost on quick successive clicks (serialize gesture press scope, don't restart pointerInput on recomposition)

* plans: document lost-click defect (press-scope race, pointerInput restart) and fixes

* multiplatform: don't cancel command click when chat list shifts under the pointer

A press is cancelled when it goes out of the node's bounds, but when a sent
message inserts into the chat the node moves out from under a stationary
pointer, which is not a drag-away. Exempt out-of-bounds cancellation when the
pointer did not move in window coordinates (within touch slop).

* multiplatform: stop pointer handler resets on chat item recomposition (lost clicks, cursor flicker)

bigTouchSlop() created a new ViewConfiguration instance on every recomposition
of every chat item; pointer input nodes observe ViewConfiguration and reset
their handler when it changes, so each inserted message killed all in-flight
presses (lost command clicks) and hover handlers (hand cursor flicker) in the
viewport. Provide a remembered instance instead.

* plans: document list-shift press cancellation and ViewConfiguration reset defects

* desktop: remove imperative hover cursor workaround, superseded by ViewConfiguration fix

The evidence that the declarative path was insufficient was gathered while
every message insertion was resetting all hover handlers (the ViewConfiguration
identity defect, fixed in 6d24bd5d4) — which alone explains those failures.
With handlers stable, the full hover matrix passes on pointerHoverIcon plus the
lossless detectCursorMove alone, so the AWT canvas write and its expect/actuals
are removed; only the icon state reset on Exit remains.

* plans: document hover workaround removal and stage-1 evidence contamination
2026-07-22 08:43:35 +01:00
Narasimha-sc 3615927db5 multiplatform: remove unused clipboard state polling that froze desktop UI (#7237)
The desktop app polled full clipboard contents every second on the AWT
event thread to update ChatModel.clipboardHasText. On X11, each read
blocks up to sun.awt.datatransfer.timeout (10s) when the selection owner
does not respond (e.g. after KeePassXC's clipboard auto-clear), making
every click/scroll wait ~9s. clipboardHasText has had no readers since
its only consumer was removed in #4398, so the whole mechanism (desktop
poll, Android clip listener, onResume refresh, expect/actual, state
field) is deleted. See plans/2026-07-11-fix-desktop-clipboard-freeze.md.
2026-07-22 08:35:41 +01:00
Narasimha-sc 7fc2a6e6bd android, desktop: fix draft loss when switching to chat where user cannot send messages (#7239)
* android, desktop: save draft when switching to chat where user cannot send messages

On desktop, chat and chatId change in the same recomposition when another
chat is opened from the always-visible chat list. The effect clearing
compose state of a non-sendable chat (observer, channel subscriber,
review by admins) was composed before the draft-saving KeyChangeEffect
and ran first, wiping the live compose state before it could be saved
and then clearing the previously saved draft via clearPrevDraft.

Effects launch in composition order, so the clearing effect is moved
after KeyChangeEffect: the previous chat's draft is saved first, and
clearCurrentDraft is a no-op for it because draftChatId no longer
matches the opened chat. Clearing the opened chat's own draft when it
cannot send is preserved, as is clearing when the open chat itself
becomes non-sendable (only the sendMsgEnabled key changes).

* plans: investigation and justification for draft message loss fix
2026-07-22 08:32:37 +01:00
Narasimha-sc 9586c97439 ui: show XFTP servers used for a file in message info (#7088)
Surface the XFTP servers that hosted a file's chunks in the message info
screen (Android, desktop, iOS), so a user can see which servers they are
downloading from (or, for sent files, uploading to) whenever they want to know.

APIGetChatItemInfo now returns fileXftpServers, derived from the stored file
description (private snd descr for sent items, rcv descr for received items);
extraction is best-effort and never fails the call.
2026-07-22 08:28:54 +01:00
shandEvgeny Poberezkin 2ea5940e81 core, ui: per-server roles for self-hosted servers (#7254)
* core, ui: plan per-server roles for self-hosted servers

* core: add per-server roles field to UserServer

* core: add nullable role columns to protocol_servers

* core: persist per-server roles

* core: validate server coverage using per-server roles

* test: cover per-server roles resolution and coverage

* multiplatform: per-server role toggles for self-hosted servers

* ios: per-server role toggles for self-hosted servers

* core: per-server role overrides with per-role defaults

* test: cover three-state per-server role resolution

* fix: derive Eq for ServerRolesOverride

* multiplatform: three-state role dropdowns on saved servers

* ios: three-state role pickers on saved servers

* test: per-server roles independent across two servers

* test: enable names role in name-resolution tests

* style: trim comments in per-server roles code

* chore: rename server_roles migration to 20260716 (last)

* core: per-server roles override operator roles, inherit when unset

* multiplatform: per-server role default inherits from operator

* ios: per-server role default inherits from operator

* refactor(servers): tidy per-server roles per review

- dedup no-operator default into ServerRoles.noOperatorDefault (Kotlin/Swift)
- iOS: move roles-section control flow to the call site via a named gate,
  and parse the server address once instead of up to three times
- Kotlin: collapse redundant derivedState; revert defaultOn->default rename
  for cross-platform parity
- drop unused Hashable conformance on Swift ServerRoles
- add agentServerCfgs test for names inheritance from an operator
- remove no-op enableNamesRole calls from dormant DirectoryTests
- fix ChatClient import ordering

* chore(migration): date server_roles migration 20260720

* only show roles when server is enabled, move section above QR code

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2026-07-20 16:35:13 +01:00
Narasimha-sc 147355b76a desktop: don't delete shared temp dirs from a transient second instance (#7168)
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.
2026-07-18 20:34:34 +01:00
Narasimha-sc c125836d85 android, desktop: fix empty area below wide images (beta.3 regression) (#7223)
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.
2026-07-18 14:58:25 +01:00
Narasimha-sc 11f0b6bd51 desktop: show startup errors in a copyable window instead of bare "Failed to launch JVM" (#7261)
* 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)
2026-07-17 15:41:54 +01:00
Narasimha-sc cbd625d57a desktop: fix "Failed to launch JVM" on Windows when Java Access Bridge is enabled (one cause of #4146) (#7260)
* 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
2026-07-16 13:49:10 +01:00
Evgenysimplex-chat-agent[bot] <287173099+simplex-chat-agent[bot]@users.noreply.github.com>Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
6375457685 core: add optional profile description (#7256)
* core: add optional profile description

* bot types

* kotlin ui

* query plans

* postgres schema

* fix ui

* fix UI

* refactor

* description in business chats

* share address

* sign address card when shared by owner

* from owner string

* remove unused string

* refactor

* fix

* ProfileDescriptionText

* refactor modals

* ios ui

* nix config

* correction

Co-authored-by: simplex-chat-agent[bot] <287173099+simplex-chat-agent[bot]@users.noreply.github.com>

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: simplex-chat-agent[bot] <287173099+simplex-chat-agent[bot]@users.noreply.github.com>
2026-07-15 17:54:21 +01:00
EvgenyandEvgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> 414f4b6ce1 core, ui: show domain for business chat in CLI, test, improve UI (#7235)
* core: preserve domain during group handshake

* query plans

* ui changes

* remove unnecessary change

* improve ui

* name UI

* card layout

* fix footers, entry field

* error icon

* fix height

* fix layout

* fix layout

* remove unused string

* focus name field

* refactor, fix

* improve button

* refactor

* fix ios race

* core: add domain to channel /i output

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-07-12 09:41:05 +01:00
spaced4ndy 51ad366e8b core, ui: message signing (#7115) 2026-07-10 19:55:10 +00:00