Commit Graph
3548 Commits
Author SHA1 Message Date
Narasimha-sc 02eaa882ac fix the docs the API merge invalidated, and use the async listUsers
Merging keepActiveUser into apiCreateActiveUser changed a signature that two Kotlin
docs quote. spec/api.md still described the old parameters and said the command sets
the profile as active, full stop; product/flows/onboarding.md quoted the old signature
one step above the line this branch had already updated to mention the flag. The iOS
spec table was fixed in the previous commit and these were missed - the same drift on
the other platform.

Both iOS call sites now use listUsersAsync, which is what changeActiveUserAsync_ uses
a few lines later; the synchronous listUsers blocks whatever executor the Task is on,
and SwiftUI infers that as the main actor.

Not compiled: the Kotlin changes here are markdown only, and the Swift ones cannot be
built on this machine.
2026-08-06 17:49:37 +00:00
Narasimha-sc 2a7eb7eed2 fix the review's findings and cut the remaining API surface
Correctness.

The one-time link picker's profile list was keyed on the active user as well as the
count. That defeats the "don't change order after a user was selected" behaviour its own
comment protects - changeActiveUser_ sets currentUser first and only then reloads chats,
so the list visibly re-sorted and stayed re-sorted for the whole of getUserChatData - and
it was redundant: the count alone covers a profile being created from the picker.

iOS, compose picker: the old-core fallback relied on the user switch tearing this view
down, which only happens when the switch succeeded. If it threw, the form was left open
over a profile that already existed, and the only way out was to swipe it away. It is now
dismissed unconditionally.

iOS: alerts raised right after a sheet is dismissed are presented on a controller that is
going away and are dropped. The 0.5s wait was applied only to the old-core path, while the
ordinary reassignment failures - the ones that actually happen - went unguarded.
alertAfterDismissal now covers all of them, in one place.

iOS: dropped the chatId write that Kotlin removed for the same reason two commits ago. It
is a no-op on the happy path, but not if something legitimately closed the chat meanwhile
- SimpleX lock re-auth clears it, and this would reopen the chat behind the lock screen.

iOS: the create-profile row is now gated on profileChangeProhibited, as every other row on
both platforms already was.

Surface.

apiCreateActiveUser takes keepActiveUser as a defaulted argument, exactly as it already
takes pastTimestamp, instead of a public wrapper plus a private helper. Two symbols fewer
on each platform, and every existing call site passes its arguments by name, so none of
them changes.

Reverted the activeOrder sort flip in the compose picker: it changes the order of an
existing screen for every user, is not needed for this feature, and with active_order 0
would push a newly created profile to the bottom. It deserves its own commit if wanted.

Also: apps/ios/spec/api.md tracks the command enum and had gone stale; the plan records
the two limits of the flag the review surfaced - it is ignored when there is no active
user, and active_order 0 ties rather than sorts last on migrated databases.
2026-08-06 17:27:35 +00:00
Narasimha-sc 937e688c0a android, desktop, ios: make the invitation profile flow surgical
No behaviour change. The review rounds left the flow carrying their own reasoning:
267 of the 759 added lines were comments, and the fixes had accreted structure that
was no longer needed.

createProfileForInvitation runs on Main via withApi, as the pickers' own handlers
always did, instead of withBGApi with three withContext(Dispatchers.Main) blocks
inside it. Every call it makes suspends into IO - sendCmd hops there itself - so Main
is never blocked, and the whole class of off-main model mutation that two rounds of
review found simply cannot arise. That removes the two coroutine imports as well.

hasModalOpenNotClosing is gone: while the form is open it is necessarily the last
modal on the fullscreen stack, so the entry guard uses isLastModalOpenNotClosing and
one function covers both call sites.

The rest is comments. Kept the ones that record why something non-obvious is the way
it is - the placement, the main-thread choice, the guards - and dropped the narration
of what the earlier attempts got wrong, which belongs in the history and the plan.
One comment described code that is not there at all; it is the commit message's job
to say why the chatId write was removed.

759 -> 667 added lines, 267 -> 189 of them comments.
2026-08-06 17:03:40 +00:00
Narasimha-sc b9eb86541c ios: don't capture a mutable local in a @Sendable closure
MainActor.run's body is @Sendable, and the switched flag added in the previous commit
was a var captured by it - which strict concurrency diagnoses. Assigning a let on both
branches of the do/catch is definitely initialised and safe to capture anywhere, with
no behaviour change.
2026-08-06 16:12:26 +00:00
Narasimha-sc 147955f52d ios: fix the fallback ordering and close the input window on both pickers
Third review pass. Still not compiled - no Swift toolchain here.

The compose picker's fallback had moved the chatId clear out of the do block, so it
fired when the switch had FAILED - closing a prepared chat that was still perfectly
valid and still owned by the active profile. It is now conditional on the switch having
happened.

Dismissing the form before the switch did not help either: the dismissal animates for
about a third of a second, getTopViewController() keeps returning the sheet until it
ends, and the local database work finishes sooner - so the alert was presented on a
controller being dismissed and dropped, on the common path. The explicit dismissal is
gone (the switch removes this view and its sheet anyway) and the alert is deferred past
the transition.

The one-time link picker's rows had no guard at all: allowsHitTesting keyed on
switchingProfileByTimeout, which latches half a second late, so a second row was
tappable for that whole window right after the form closed - starting a second
connection change on a pccConnId the first was recreating. Both pickers now block input
from the moment the work starts.

In the compose picker the guard is on the branches that start work, not on the whole
Button, so expanding and collapsing the list - which is local - still works, and a tap
in a chat where the profile cannot be changed still explains itself.

Also: that picker's other-profiles list sorted activeOrder ascending while every other
list on both platforms sorts descending, which the core change made visible by giving a
never-activated profile order 0; the one-time link picker's fallback now dismisses and
resyncs its selection as the compose picker does; and the incognito handler's new else
no longer writes incognitoEnabled, which is bound to the app-wide default and marks the
invitation as used.
2026-08-06 15:32:17 +00:00
Narasimha-sc f76b0b3efd android, desktop: guard the picker's own dismissal and drop the redundant chatId write
Third review pass on this flow. Four things, all in code the previous two passes
touched.

The one-time link picker's close(). selectProfileAsync ended with an unconditional
close(), which pops whatever is on top of the start stack. The reassignment before it
can wait indefinitely on the retry alert, and back is not blocked while it does - so
backing out and then letting the call complete dismissed the New chat screen as well.
The picker now has its own ModalViewId and only closes if it is still the top, which is
what isLastModalOpenNotClosing was added for; the create-profile flow was using it and
this, the older hazard, was not.

The entry guard used hasModalOpen, which counts a modal that is still animating out, so
re-opening the form straight after backing out of it was a silent no-op for the length
of the animation - the exact bug the NotClosing variant exists for. Adds the matching
hasModalOpenNotClosing.

The old-core fallback switched the active user and returned without closing the form,
leaving it on top of a chat list belonging to a different profile, and did it off the
main thread. It now closes first and runs on Main like the rest of the flow.

chatModel.chatId is no longer written after the switch. changeActiveUser_ passes
keepingChatId and updateChats only clears chatId when the chat is missing from the
reloaded list, so on success the write was a no-op - and in the one case the guard does
fire it re-pointed the chat view at a chat that is not there. It also undid a
notification tap that had navigated to another chat of the same profile, which the
active-user check cannot catch.

Also restores the profileChangeProhibited guard that the previous commit dropped from
the new row while collapsing its click handler; keys the one-time link picker's profile
list on the active user as well as the count, so the checkmark is not stale after a
switch that leaves the count unchanged; dims the compose picker while it is busy, which
was the only picker with no indication at all; and logs the silent abort.
2026-08-06 15:31:55 +00:00
Narasimha-sc 49a8cafa94 ios: fix the fallback paths and guard the picker during the reassignment
Follow-up to the previous iOS commit. Still not compiled - no Swift toolchain here.

The "core ignored keepActiveUser" path in the compose picker rested on a false
premise. It claimed the form stays open and the alert draws over it, but the resync it
performs replaces the chat list with the new profile's, and the prepared chat stayed
with the previous one - so getChat returns nothing, the pushed chat view renders
nothing, and this picker, which is the sheet's presenter, is removed by the switch
itself. The alert was then raised into a hierarchy being torn down and discarded. The
form is now dismissed first, and the chat id cleared only if it is still the chat this
picker belongs to, since a notification tap may have navigated elsewhere meanwhile.

Rows stay live during the reassignment. creatingProfile is cleared as soon as
createProfileForChat returns, which is the moment it hands off to changeProfile's
fire-and-forget Task, and none of the rows had a disabled state at all - so a second
change could be started on top of the first, two concurrent reassignments of the same
prepared contact and two racing user switches. This is what the Kotlin side made
onCreated suspending for.

The one-time link picker's new else branch reset the switch status but not the
selection, so after a cancelled retry the profile that was not switched to kept the
checkmark and tapping it did nothing - the view thought it was already selected. Its
"core ignored the flag" path likewise left the picker pointing at a profile that is no
longer active. The incognito handler has the identical latch the else branch was added
for, and hardening the row's disabled state on profileSwitchStatus made that stuck path
disable the row permanently, so it gets the same treatment - and its two state writes
move onto the main actor, where the rest of that block already was.

Both swallowed errors are now logged, and the comment claiming two .sheet modifiers on
one view conflict is dropped - it contradicted the sibling file and the deployment
target.
2026-08-06 14:17:37 +00:00
Narasimha-sc 60cc62ee3c android, desktop: show the create-profile form over every pane
Follow-up to the previous commit, which fixed the centre-pane placement by moving
the form to the picker's own pane. That was wrong in two different ways, and the
shared modal helpers it changed had a blast radius that was not accounted for.

Placement. ModalManager.end, used for the compose picker, puts the form in a pane
*beside* the chat: the picker stays fully live next to it, with no scrim, so a profile
can be selected while the form is open - and apiChangeConnectionUser recreates the
connection, invalidating the id the form is about to use. It also calls
desktopExpandWindowToWidth, which is grow-only, so the window is permanently widened.
ModalManager.start, used for the one-time link picker, does cover it, but disposes it,
losing whatever was typed in its search box. ModalManager.fullscreen is an opaque
Surface over every pane: nothing can be operated while the form is up, nothing is torn
down, chatId is untouched and the window does not move. The manager is no longer a
parameter - there is only one right answer.

Modal helpers. hasModalOpen/isLastModalOpen were taught to ignore modals waiting out
their close animation, but they have five other callers - ChatListView, ChatView twice,
ChatItemView and SimpleXAPI - all deciding when to tear down secondary chats, and all
silently changed by it. isLastModalOpen also went from an indexed read to a full
iteration on a path called off the main thread. Both are reverted; the new behaviour
lives in isLastModalOpenNotClosing, which only this flow calls and which walks by
index.

Main thread. Making onCreated suspending moved the reassignment onto withBGApi's
single-thread dispatcher, so changeProfileTo mutated chatsContext, chatId and (via
changeActiveUser_) chatModel.users off Main, where the receiver loop writes the same
structures - the very hazard the same commit called out for its own users refresh.
onCreated and close() are now handed back to Dispatchers.Main, which is where both
pickers ran this before it became a callback.

Also: the in-flight flag moves to ChatModel so it is not a top-level val first created
inside a composition; the entry guard consults it, since with one manager per pane the
per-manager check alone let a second form open and silently drop its submit; the
one-time link picker's filteredProfiles is keyed on the profile count, so a profile
created from it appears without relying on the composition being rebuilt; and the
progress indicator skips its 500ms grace while creating, since that timer restarts
from zero in the rebuilt composition and would otherwise leave the rows looking idle.
2026-08-06 14:17:17 +00:00
Narasimha-sc 4a169a3061 ios: fix the create-profile-for-invitation flow
Mirrors the Android/desktop fixes where iOS has the same defects, and fixes the ones
that are specific to SwiftUI. Still not compiled - no Swift toolchain here, same
status as the rest of the iOS port in this branch.

Swallowed alert. The one-time link picker reported "the core ignored keepActiveUser"
by setting its own `alert` @State, consumed by .alert(item:) on the very view that is
presenting the create-profile sheet. UIKit refuses an alert on a controller that
already has a presentation, so it was never shown: the active profile silently
switched, the connection stayed where it was, and the user was left looking at an
unchanged form. Uses the global showAlert, which targets the top view controller, as
the compose picker already does.

Stale picker list. The compose picker fills its own `users` in onAppear and never
again, so the profile just created was missing from the list whenever the
reassignment failed - tapping "Add profile" again then hit "Duplicate display name!"
- and the frame height, which is computed from that count, was one row short. The
local from listUsers also shadowed the property, which is what hid this.

Resync failure reported as a creation failure. changeActiveUserAsync_ throws, and on
the "core ignored the flag" path that propagated into the form's catch, which says
"Error creating profile!" although the profile had been created. On the compose
picker that path also left chatId pointing at a prepared chat that had stayed with
the previous profile, i.e. at a chat absent from the list, which renders blank.

Sheet owner destroyed mid-dismissal. The compose picker's sheet was attached inside
profilePicker(), which is swapped out for currentSelection() the moment listExpanded
flips - which changeProfile does while the sheet is still dismissing, and which an
incoming event can do at any time via profileChangeProhibited. Moved to the Group in
viewBody(), which survives both; stacking sheets is supported from iOS 14.5 and the
app targets 15.

Dismissal during creation. The submit runs in an unstructured Task that SwiftUI does
not cancel, so a swipe-to-dismiss still created the profile and switched to it while
every state write, including the one clearing the in-flight flag, landed on a view
that was gone. Both sheets now block interactive dismissal while creating.

Latched picker. If apiChangeConnectionUser returns nothing rather than throwing,
profileSwitchStatus was never reset, leaving the picker dimmed behind a spinner with
hit testing off. Pre-existing, but the new row routes through the same handler.

Re-tappable row. switchingProfileByTimeout only latches half a second after the
switch starts, and creatingProfile is cleared as soon as the switch is handed over,
so the row was live in between.
2026-08-06 13:45:11 +00:00
Narasimha-sc f2f5ab3892 android, desktop: fix the create-profile-for-invitation flow
Six defects, all in the window between creating the profile and moving the
invitation onto it.

Pane. The form was shown in ModalManager.center, which on desktop sets
ChatModel.chatId to null - and the chat view *is* the centre pane. Tapping "Add
profile" in a prepared invitation therefore closed that chat, discarding the typed
compose draft with it, and only the success path reopened it: cancelling the form,
or any failure, left "No selected chat" with the invitation nowhere on screen. From
the one-time link picker, which is itself a start-pane modal, the form appeared in a
different pane with the picker still live beside it and no scrim, so a profile could
be selected while the form was open - and apiChangeConnectionUser recreates the
connection, invalidating the pccConnId the form was about to use. The manager is now
passed in: end for the compose picker, matching the incognito modal it already opens
there, and start for the one-time link picker, matching its own.

In-flight guard. The flag was per-picker and released when the profile existed, not
when the invitation had moved. Both are wrong: onCreated only launches the
reassignment, and on Android the picker's composition is disposed while the form is
on top of it, so the flag it comes back with is a fresh false. There was no spinner,
no dimming and every row was live during the switch, so a second tap created a second
profile or started a competing reassignment. onCreated is now suspending so the flag
covers the whole flow, and the flag is a single top-level state both pickers read.
The compose picker had no in-progress state at all and now has one, and its new row
was also the only one missing the profileChangeProhibited guard.

Active user and host. The reassignment resolves the invitation under whatever profile
is active when it runs. A notification tap switches the active user from another
dispatcher, and a remote host connect/disconnect switches the host, so both are
checked before handing over rather than reassigning under the wrong profile.

Torn-down form. isLastModalOpen was used only to decide whether to close the form,
and the flow continued when it was false - reassigning and switching under a screen
the user had already left. It now stops.

Connection not moved. The one-time link picker dismissed itself and turned the
app-wide incognito default off whether or not apiChangeConnectionUser succeeded.
Unlike the prepared-chat reassignment, that call provisions a new queue, so it is
what fails offline - leaving a profile just created for the invitation stranded with
nothing pointing at it and the picker gone. It now keeps the picker open and only
clears incognito once the connection has actually moved.

Reopening the chat. chatModel.chatId was set unconditionally after the switch, before
the check that the switch actually happened, pointing the chat view at a chat that had
just moved to another profile and defeating the guard in updateChats that clears it.

Also applies the users refresh on the main thread, where the receiver loop updates
the same list, and skips it when the host has changed underneath.
2026-08-06 13:45:11 +00:00
Narasimha-sc 4ced18a728 android, desktop: don't treat a modal that is closing as still open
closeModal leaves an animated modal in modalViews and only stages its index in
toRemove, drained when the animation ends. hasModalOpen and isLastModalOpen scanned
modalViews directly, so for the ~250ms of the close animation they still reported a
dismissed modal as open.

A caller that closes on "is my modal still the last one?" therefore popped the modal
underneath as well - on Android, where every placement shares one stack, backing out
of the create-profile form while creation was in flight dismissed the New chat screen
too. Both checks now skip the entries already staged for removal.
2026-08-06 13:34:37 +00:00
Narasimha-sc ef703ec0be ios: offer creating a profile for a one-time invitation link
Adds "Add profile" to the picker reached from "Share profile" on a one-time
link. The new profile is routed through the existing selectedProfile path, so
the connection change and the switch cannot drift from picking an existing one.

The row sits outside the active-profile branch: inside it, it would disappear
whenever the active profile is filtered out by the search text.
2026-08-03 16:12:10 +00:00
Narasimha-sc 9a69662970 ios: offer creating a profile when accepting an invitation
Adds "Add profile" to the profile picker shown above the compose box for a
prepared chat, so connecting as someone new does not mean leaving the invitation
to create a profile in settings.

NewUser and createActiveUser gain keepActiveUser, with apiCreateProfileKeepingActive
as the named entry point; the flag never appears at a call site. The profile is
created without becoming active so the profile that owns the prepared chat stays
active for the reassignment.

The row is emitted first: unlike the Android/desktop list this one is not
reverse-laid-out, so first renders at the top. After switching, the chat is
reopened explicitly - keepingChatId only preserves its place in the reloaded
list.

Reuses the existing "Add profile" and "Error changing chat profile" strings, so
no new translation entries.
2026-08-03 16:12:10 +00:00
Narasimha-sc a570d56ece ios: parameterise create-profile form's submit action
CreateProfile hardcoded create-and-activate. Adds an optional onSubmit hook so
the same form can create a profile for an invitation, which must not switch the
active user until the prepared chat has been reassigned.

Optional rather than required, unlike the Kotlin equivalent: it touches no
existing call site, which matters more here because this cannot be compiled on
the machine the change was written on.
2026-08-03 16:12:10 +00:00
Narasimha-sc eab9e36300 desktop: fix profile image lost when creating the first chat profile
createProfileInNoProfileSetup passed the image positionally as Profile's 4th
argument, which is description, not image - so the chosen avatar was stored as
the profile description and no picture was set.

Both are String?, so it type-checked silently. The sibling createProfileInProfiles
avoids this by passing image by name.

Only reachable on desktop with no local chat profile: the user picker then offers
"Create chat profile" (desktopNoUserNoRemote), which opens the profile form with
its avatar picker, and localUserCreated is false so submission takes this branch.
Onboarding is unaffected - both first-profile screens take a name only, with no
avatar picker, and call createProfileOnboarding instead.

Unrelated to the rest of this branch - safe to drop or move to its own PR.
2026-08-03 16:12:10 +00:00
Narasimha-sc 49344c32fb android, desktop: offer creating a profile for a one-time invitation link
Adds "Add profile" to the profile picker reached from "Share profile" on a
one-time link, so a link can be handed out under a new identity without setting
one up in settings first. Reuses the existing users_add string.

Extracts the existing selection handler into selectProfile so the new profile
takes exactly the same path as picking an existing one - the connection change
and the switch cannot drift apart.

The row sits outside the active-profile branch: inside it, it would disappear
whenever the active profile is filtered out by the search text. It is only
offered when there is a connection to move, so it does not appear in the share
list, where a new profile would have nothing to share into.
2026-08-03 16:12:10 +00:00
Narasimha-sc c270a1531d android, desktop: extract profile selection from the picker row handler
ActiveProfilePicker performed the whole profile change - connection reassignment,
switch, alerts, close - inside the row's onSelected lambda, where nothing else can
reach it.

Lift it to selectProfile(user), called by the row. Pure move: the body is unchanged
apart from indentation, so picking a profile behaves exactly as before. It gives the
next commit a way to take the same path for a newly created profile rather than
duplicating it.
2026-08-03 16:12:10 +00:00
Narasimha-sc b0b852a10f android, desktop: offer creating a profile when accepting an invitation
Opening someone's link prepares a chat and shows the profile picker above the
compose box. It listed existing profiles and Incognito, so connecting as
someone new meant leaving the invitation, creating a profile in settings, and
coming back to the link.

Adds "Add profile" to that picker, reusing the existing users_add string. The
profile is created without becoming active, so the profile that owns the
prepared chat stays active for the reassignment, which then switches once -
rather than switching away and back.

The row is emitted last, so with the reversed layout it renders at the top of
the expanded list with the current selection nearest the compose box.

After switching, the chat is reopened explicitly: keepingChatId only preserves
its place in the reloaded list, so without that the switch lands on the chat
list of the new profile rather than the invitation it was chosen for. This
applies to picking an existing profile too.

The form is only dismissed once creation succeeds, so a rejected duplicate name
does not discard what was typed. A dismissal guard keeps a back-tap during
creation from switching the profile anyway, and the created profile is put into
the picker list immediately so it stays visible if the reassignment fails.

If the core activated the profile despite keepActiveUser - an older remote host
ignoring the field - the reassignment would fail, so that case resyncs to what
the host actually did instead.

Updates the onboarding flow doc, which names the arguments of the command that
gains the field.
2026-08-03 13:12:00 +00:00
Narasimha-sc c38c87e26a android, desktop: don't switch profile when prepared chat reassignment fails
changeActiveUser_ was called outside the guards that check whether
apiChangePreparedContactUser / apiChangePreparedGroupUser actually succeeded.
When the reassignment failed the app still switched to the other profile,
leaving the invitation behind in the original one - and keepingChatId then held
a chat belonging to a different user.

Reachable today: open a link, expand the profile picker, and have the connection
start in the window before tapping. profileChangeProhibited flips to true, the
API returns null, and the switch happened anyway.

The sibling picker in NewChatView already guards this correctly. iOS is
unaffected: there apiChangePreparedContactUser throws, so a failure skips the
switch by control flow.
2026-08-03 07:59:49 +00:00
Narasimha-sc 7c6f046b58 android, desktop: parameterise create-profile form's submit action
CreateProfile hardcoded what happens after submission, choosing between
createProfileInProfiles and createProfileInNoProfileSetup inside the button
handler. That conditional is a hidden premise: two call sites invoke the
composable and neither can see from the call that submission behaves
differently for them.

Lift the choice to the callers. The callback passes raw fields rather than a
Profile because the two paths do not build the same one - the no-profile-setup
path drops shortDescr and passes a null remote host id - so a Profile-shaped
callback would silently change its behaviour.

The chatModel parameter was redundant besides: UserPicker passed the
platform-level chatModel global, which the parameter merely shadowed.

No behaviour change - the conditional moves verbatim into createProfileFromForm,
which both call sites invoke, rather than being copied into each.
2026-08-03 07:59:49 +00:00
EvgenyandGitHub 34b74d1c49 directory: remove support for directory log file (#7331)
* directory: remove support for directory log file

* remove unused exports
2026-08-02 16:01:22 +01:00
EvgenyGitHubEvgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>Evgeny @ 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
EvgenyGitHubEvgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
61012d208e core, ui: extend profile peer type (#7277)
* core, ui: extend profile peer type

* remove comments

Co-authored-by: Evgeny <evgeny@poberezkin.com>

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-07-31 23:53:12 +01:00
SimpleX ChatandEvgeny Poberezkin e11128ce5b 7.0: android 366, desktop 155, ios 345 2026-07-28 16:41:46 +01:00
Narasimha-scandGitHub bd298a1d4a ios, android, desktop: fix draft in support chat appearing in group chat (#7309)
Group chat and its support chats have the same chat id, so a draft saved
in "chat with admins" was restored in the group chat and shown as its draft
in the chat list. Include chat scope in the draft chat id.

Also save the draft when support chat is closed without changing chat id -
in that case KeyChangeEffect that saves drafts doesn't run.
2026-07-28 13:55:49 +01:00
spaced4ndyandGitHub 7c931d08ea ui: show "connect to name" button starting with 5 characters; ios: show button in New chat view search (#7318)
* ui: show "connect to name" button starting with 5 characters

* show button in new chat view search
2026-07-28 12:03:42 +01:00
shGitHubmlanpEvgeny PoberezkinHosted Weblate user 54392No nameEvgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>Narasimha-sc
a58cd51c49 ui: translations (#7317)
* Translated using Weblate (German)

Currently translated at 100.0% (2899 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/

* Translated using Weblate (Russian)

Currently translated at 97.5% (2829 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.9% (2898 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (2899 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (2497 of 2497 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/

* ru translations

* Translated using Weblate (German)

Currently translated at 100.0% (2899 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/

* Translated using Weblate (Russian)

Currently translated at 97.5% (2829 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.9% (2898 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (2899 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (2497 of 2497 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/

* process translations

* ui: fix translation errors in es, ru, de, it, hu

Review of the translations in this PR against the English originals.
Values only - no keys added, removed or renamed.

Spanish:
- "The sender deleted the connection request." said "El servidor..."
  (the server), attributing the deletion to a server rather than the
  sender. Restored "El remitente".
- "Require/Do not require signing messages." were rendered as statements
  ("Se requieren mensajes firmados."), making the toggle options read
  identically to the separate status strings "Message signing is
  (not) required.". Restored the imperative.
- "...compare (or scan) the code on your devices." said "en tu
  dispositivo" (singular); the code has to be compared on both.
- "How to register a test name" was pluralised to "nombres en pruebas".

Russian:
- "Помошь и поддержка" -> "Помощь и поддержка" (spelling).
- "File servers" was the calque "Файл серверы" -> "Серверы файлов",
  matching the iOS translation of the same string.
- "Если Вы выберите" -> "выберете" (imperative used for future).
- "Create web preview." said "Создать веб страницу." (web page, also
  missing the hyphen) -> "Создать веб-предпросмотр.".
- webpage_info dropped "to show your channel preview to visitors before
  they subscribe"; webpage_url_footer said "страницы канала" instead of
  the preview. Both restored from the iOS wording.
- "Enter description (optional)" lost "(optional)"; "Error adding
  relays" was truncated to "Ошибка добавления".
- "Add contributors." used "авторов"; the member role is "соавтор".
- "удален(а)" -> "удалён", "подтвержденный" -> "подтверждённый".

German, Italian, Hungarian:
- The source strings "Names for your channel or business." and
  "SimpleX names (BETA)" were renamed to "Public names..." and
  "SimpleX public names (BETA)", but the old values were carried over,
  so "public" was missing. Android German and Chinese had already been
  updated in this PR; this brings iOS German, and both platforms for
  Italian and Hungarian, in line.
- iOS German "How to register a test name" still read "Einen Test-Namen
  registrieren", while Android German was updated in this PR.

---------

Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Evgeny Poberezkin <e.poberezkin@me.com>
Co-authored-by: Hosted Weblate user 54392 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: No name <usir.alerts@onionmail.org>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com>
2026-07-28 11:26:04 +01:00
Narasimha-scandGitHub 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
spaced4ndyandGitHub b414d7579a ios: fix connection dialogue not shown if app is opened via link before onboarding (#7301)
* ios: fix connection dialogue not shown if app is opened via link before onboarding

* wip

* debug

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* comment
2026-07-27 18:00:32 +01:00
spaced4ndyandGitHub 6152f2a7ee ios: fix chat not opening when contact connects via 1-time link (#7315) 2026-07-27 15:04:45 +00:00
spaced4ndyandGitHub 45ee868e0b android, desktop: fix reply header not filling message width for emoji (#7314) 2026-07-27 14:47:05 +00:00
Narasimha-scandGitHub 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
Evgeny Poberezkin 4b97110fd9 7.0.0-beta.6: android 365, desktop 154 2026-07-25 18:16:23 +01:00
Evgeny Poberezkin 15ef21de0c ios: build 344 2026-07-25 17:17:39 +01:00
Evgeny Poberezkin 09254c5a53 ui: update whats new 2026-07-25 16:14:22 +01:00
+16 8c80e4fd57 ui: translations (#7305)
* Translated using Weblate (German)

Currently translated at 100.0% (2897 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/

* Translated using Weblate (German)

Currently translated at 100.0% (2495 of 2495 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/

* Translated using Weblate (French)

Currently translated at 82.3% (2387 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/

* Translated using Weblate (Italian)

Currently translated at 100.0% (2897 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/

* Translated using Weblate (Italian)

Currently translated at 100.0% (2897 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/

* Translated using Weblate (Italian)

Currently translated at 100.0% (2495 of 2495 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.9% (2896 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 96.7% (2803 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hant/

* Translated using Weblate (Dutch)

Currently translated at 82.2% (2382 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/

* Translated using Weblate (Japanese)

Currently translated at 96.9% (2810 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ja/

* Translated using Weblate (Czech)

Currently translated at 93.6% (2712 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 80.2% (2325 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2897 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/

* Translated using Weblate (Ukrainian)

Currently translated at 91.8% (2660 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/

* Translated using Weblate (Lithuanian)

Currently translated at 58.5% (1696 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/lt/

* Translated using Weblate (Finnish)

Currently translated at 49.2% (1427 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fi/

* Translated using Weblate (Korean)

Currently translated at 50.5% (1463 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ko/

* Translated using Weblate (Polish)

Currently translated at 86.8% (2516 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pl/

* Translated using Weblate (Hebrew)

Currently translated at 71.0% (2058 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/he/

* Translated using Weblate (Thai)

Currently translated at 42.5% (1233 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/th/

* Translated using Weblate (Bulgarian)

Currently translated at 85.4% (2476 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/bg/

* Translated using Weblate (Turkish)

Currently translated at 98.7% (2862 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/tr/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2897 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/

* Translated using Weblate (Persian)

Currently translated at 84.9% (2461 of 2897 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fa/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2495 of 2495 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2899 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.9% (2898 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/

* Translated using Weblate (Italian)

Currently translated at 100.0% (2899 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/

* Translated using Weblate (Italian)

Currently translated at 100.0% (2495 of 2495 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/

* Translated using Weblate (German)

Currently translated at 100.0% (2899 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/

* Translated using Weblate (German)

Currently translated at 100.0% (2495 of 2495 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2899 of 2899 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/

* process translations

* ui: fix translation errors found in review

Android (strings.xml):
- de: "Fehler beim SimpleX-Name" -> "-Namen" (dative of Name)
- de: connection_error_auth title updated to match new EN
  "Connection link removed"; fix reflexive "Um Sie" -> "Um sich"
- hu: typo "egyszer használató" -> "használható"
- hu, it: connection_error_auth title updated to match new EN
- ar: "استيثاق الجهاز غير مفعّلة" -> "مفعّل" (masculine agreement,
  stale after the المصادقة -> الاستيثاق terminology change)
- ar: "الذين شاركَ الرابط معهم" -> "شاركتَ" (subjectless verb)
- ar: "سيظل جميع جهات اتصالك متصلين" -> "ستظل ... متصلة" (x2,
  جهات is feminine plural)
- ar: add missing trailing period to v7_0_channels_relays
- ar: passive voice for skipped-message counters, matching EN
- zh-rCN: connection_error_auth reworded as a title, not a
  subjectless clause; restore the \n dropped from _desc

iOS:
- de: same dative fix in Localizable.strings and de.xliff
- de, hu, it: add missing File servers / "File servers: %@"
  targets, ported from the Android translations in this PR

---------

Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: Deleted User <noreply+6980@weblate.org>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate user 54392 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Olivia Ng <uloo592@gmail.com>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: tomato potato <tomato.games@protonmail.com>
Co-authored-by: M Sarmad Qadeer <msarmadqadeer@gmail.com>
Co-authored-by: HypocrisyCollector <work.j6nnu@slmail.me>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Maksym Lukashenko <livelmaxim@gmail.com>
Co-authored-by: Anonimas <anonimas3@users.noreply.hosted.weblate.org>
Co-authored-by: petri <pkajander@gmail.com>
Co-authored-by: 5olivetree <5olivetree+github.com@mailbox.org>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: Roee Hershberg <roihershberg@protonmail.com>
Co-authored-by: Titapa (PunPun) Chaiyakiturajai <titapapunne@gmail.com>
Co-authored-by: elgratea <weblate@fastmail.com>
Co-authored-by: Kaanito <kaanpeker196@gmail.com>
Co-authored-by: summoner001 <summoner@disroot.org>
Co-authored-by: Siavash <ciavash@proton.me>
Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2026-07-25 16:07:00 +01:00
Evgeny Poberezkin 83bf107804 ios: update core library 2026-07-25 15:51:21 +01:00
spaced4ndyandGitHub c7e83d88ae desktop: fix opening contact request leaving previous open chat in connecting state (#7302) 2026-07-25 14:47:31 +01:00
Narasimha-scandGitHub 6a34b5e052 support bot: fix double invite when joining a customer chat (#7129)
addOrFindTeamMember added the team member as Member and then immediately
called apiSetMembersRole(Owner). For a not-yet-accepted (GSMemInvited)
member the core re-sends the group invitation on a role change, so every
/join and /team produced two invitations. Add the member directly as
Owner in a single apiAddMember call; onMemberConnected still re-asserts
Owner idempotently as a fallback.
2026-07-25 11:27:06 +01:00
8bcb9d048b directory: expand service README (#6701)
* directory: expand README with build, run, and command reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* directory: remove system packages from README prerequisites

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* directory: README fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* directory: standalone web page

* directory: Improve README.md

* directory: fix README inaccuracies, complete flags, expand self-hosting docs

- correct /filter name-filter semantics (long/blocked names, not "no name");
  note it needs --profile-name-limit / --blocked-*-file to do anything
- list the remaining directory flags and note core chat options + --help
- add the required cabal.project.local OpenSSL step; use `cabal list-bin`
- hosting page: copy files out of the repo (directory.js is a symlink, data
  URL is a top-level const), provide /img/group.svg, promoted.json unused
- bootstrap: bot prints/creates its contact address on startup
- document --owners-group / --directory-file (+ --migrate-directory-file)
- redraw lifecycle diagram; Active->PendingUpdate on link removal vs
  Active->PendingApproval on other profile changes; SuspendedBadRoles auto-restore

* directory: clarify group lifecycle diagram

Linear forward-path diagram (invitation -> Active), with transitions out
of Active as a list instead of cramped back-arrows.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-25 11:26:22 +01:00
Narasimha-scandGitHub 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
spaced4ndyandGitHub 5c196d0e2d ui: rename "how to register name" button (#7300) 2026-07-24 15:25:52 +00:00
spaced4ndyandGitHub 08711dbc33 ios: fix app crashing after onboarding (#7299) 2026-07-24 14:47:16 +00:00
spaced4ndyandGitHub 3755dc43be ios: fix receipts toggles in user settings (#7298) 2026-07-24 14:33:54 +00:00
spaced4ndyandGitHub 53daafa675 ios: put "connect to name" button below search in top bar layout (#7297) 2026-07-24 14:19:52 +00:00
fc28914455 desktop: fix saving files with '[', '*', or '?' in name on Linux (#7290)
* desktop: fix saving files with '[', '*', or '?' in name on Linux

JFileChooser's BasicFileChooserUI treats filenames containing
'[', '*', or '?' as glob patterns on Unix when the file does not
exist on disk. The Save button's ApproveSelectionAction then
applies a GlobFilter and returns without saving — the dialog
stays open and nothing is written.

Regression introduced in #2789 (commit 02d00944f, 2023-07-28),
which started pre-populating the filename via selectedFile =
File(filename). Before that the save dialog ran in
DIRECTORIES_ONLY mode and the JDK glob check never saw the
filename.

Fix: on Linux save dialogs, find the Save button by identity
(BasicFileChooserUI.getApproveSelectionAction() is public and
returns the exact ActionListener wired on the button) and
replace it with a wrapper that delegates to the original
action for non-glob names and calls approveSelection()
directly for glob names.

Windows (per the JDK only '*' and '?' are glob chars there)
and macOS (uses AWT FileDialog) are unaffected. Open dialogs
keep glob filtering for power-user search.

* desktop: always bypass JFileChooser glob-on-save on Linux

Earlier in this branch we added a heuristic to delegate to the
original ApproveSelectionAction for non-'[' names so that typing
'*.pdf' in the save dialog would still glob-filter the listing.
That preserved a Swing-only quirk that no native OS save dialog
implements:
  - macOS uses NSSavePanel (already used in this app via AWT
    FileDialog) — no glob-on-save.
  - Native Windows / Linux GTK / KDE save dialogs — no glob-on-save.
  - JFileChooser on Windows — has it, but '*' and '?' aren't even
    legal NTFS filename chars, so users won't be typing them.

It also blocks legitimate filenames containing '*' or '?' on Linux
(legal on POSIX filesystems) from being saved, since they too get
intercepted as glob.

Replace the Save button's action with a handler that always treats
the typed text as a literal filename. The result: any filename can
be saved on Linux, behaviour aligns with macOS and with every
native OS save dialog. The open dialog is untouched, so '*.pdf'
glob-filtering there still works.

* desktop: restore directory-only save dialog on Linux

The previous commit installed the literal-filename glob bypass for
every Linux save dialog (!isLoad && isLinux), including the
DIRECTORIES_ONLY mode used when filename == null — e.g. the
"Save QR code as image" flow (saveTempImageUncompressed →
saveDialog.awaitResult() with no params).

In that mode the bypass broke directory selection entirely:
selecting a subfolder + Save traversed into it instead of approving
it, and Save with an empty filename field no-oped. Net result:
QR-code image saving was unusable on Linux.

The glob-on-save bug only arises because a filename is pre-populated
(selectedFile = File(filename)), which never happens in
DIRECTORIES_ONLY mode. Gate the bypass on filename != null so the
directory-save path keeps JFileChooser's original approve action.

* desktop: simplify Linux save glob bypass via getDefaultButton

---------

Co-authored-by: shum <github.shum@liber.li>
2026-07-23 10:18:11 +01:00
SimpleX ChatandEvgeny Poberezkin ecd1237bf4 7.0.0-beta.5: android 364, desktop 153, ios 343 2026-07-22 16:50:48 +01:00
Narasimha-scandGitHub 172b11b511 android, desktop: don't allow swipe-to-reply to a marked-deleted message (#7214)
The swipe handler is created with rememberDismissState, whose lambda is frozen
at first composition and captures a stale cItem. When a message is marked
deleted while on screen, that lambda still sees the pre-deletion item, so an
in-lambda itemDeleted check is bypassed and the quote is set, producing
SEInvalidQuote on send. Disable the swipe modifier (recomputed live each
recomposition) when itemDeleted != null so the gesture is removed entirely.
2026-07-22 13:12:23 +01:00
Narasimha-scandGitHub 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-scandGitHub 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