Commit Graph
21 Commits
Author SHA1 Message Date
Narasimha-sc 54e2fac3dd fix the incognito preference ordering, and register new profiles earlier on iOS
Move appPreferences.incognito.set(true) inside the success branch: written
before the call that can fail, it left the app-wide default on so the next
connection silently used a random profile. Mirror of the profile-path bug
fixed two blocks above; iOS does not have it.

Register the created profile in chatModel.users right after creation rather
than after the ownership guard, so a user switch mid-create cannot leave a
profile that exists in the database but in no list the app shows. Replaces
the later fallbacks, which this subsumes.

Also correct the plan on simplex-chat-client/typescript: it does import the
regenerated types package, and is insulated by a ^0.3.0 pin, not by having
its own types - adding the field there would break it.
2026-08-07 15:32:12 +00:00
Narasimha-sc 3891a12d82 add the new profile to chatModel.users on iOS too, and stop double-alerting
Both iOS fallbacks appended only to the view's own state, so when listUsers
failed the profile existed in the database but in no list the app shows -
and creating it again failed on the duplicate name. Kotlin already did this.

changeActiveUser shows its own alert, so the resync path was reporting the
same failure twice; use changeActiveUser_ and report it once.
2026-08-07 15:09:05 +00:00
Narasimha-sc b9525704c9 apply the busy gate to the whole iOS picker, not just one row
The 500ms switchingProfileByTimeout delay left every profile and incognito
row live while a change was already in flight, so a second tap could run
apiChangeConnectionUser against a connection recreateConn was deleting.
Gate hit testing on busy instead, which subsumes the per-row disabled.

Also mirror the users.append fallback from the compose picker: a failed
listUsersAsync left the new profile absent from the list it was selected in.
2026-08-07 14:16:45 +00:00
Narasimha-sc 185c6e1d2b fix four more the high-effort review found, two of them my own reverts
Two of these exist because I reverted half of a coupled pair while keeping the other
half.

iOS: apiChangeConnectionUser returns nil rather than throwing when the network retry is
cancelled, so neither branch of the selectedProfile handler ran, profileSwitchStatus
stayed .switchingUser, switchingProfileByTimeout latched and the picker was left
permanently behind its spinner with hit testing off. I removed this else branch two
passes ago believing it was only reachable with no connection - it is reachable offline,
which is a row in this branch's own test matrix, and on the create path it also strands
the profile just created. Kotlin has had the equivalent guard all along.

Kotlin: appPreferences.incognito.set(false) went back to master's position, before the
connection change - but the early return that now keeps the picker open on failure stayed.
Together those clear the app-wide default while the picker still shows Incognito ticked
and nothing was moved. Moved below the reassignment, where the early return puts it out
of reach.

changeActiveUser_ throws, and withApi does not catch, so a failure inside onCreated
reached GlobalExceptionsHandler, which silently closes a modal or clears chatId - profile
created, invitation not moved, nothing said. Caught and reported, matching the runCatching
and the safe changeActiveUser wrapper this same function already uses.

iOS: when the profile list refresh fails, the new profile is absent from users while
changeProfile sets selectedUser to it, so otherUsers filters nobody out and the row count
exceeds what frame(maxHeight:) allows - clipping "Add profile" at the top, for exactly the
single-profile user this feature is for.

Also records that Terminal/Input.hs caches any CRActiveUser as the host's current user,
which keepActiveUser makes conditional. Display-only, and the response carries nothing to
distinguish the two cases.
2026-08-07 13:05:02 +00:00
Narasimha-sc 3a437a4597 fix three gaps the targeted review found
The abort on "active user or host changed" left the create-profile form open over a
profile that had already been created, so the next Create failed on the duplicate name
for as long as the name was unchanged - a dead end with no way out but retyping. It
closes the form now, like every other terminal path in that function.

The compose picker's busy state only covered the create flow, so both the existing rows
and the new one stayed live during an ordinary profile change. Tapping a profile and then
"Add profile" reassigned the same prepared chat twice and issued two overlapping
changeActiveUser_ calls, whose chat reloads are not serialised. Master had no way to
trigger this - the new row is the third way in - so the flag comes back, but only around
changeProfile, leaving changeProfileTo untouched.

iOS never re-checked the active user between creating the profile and reassigning, which
§4 requires and Kotlin does. A notification action switching profile in that window makes
the reassignment resolve under the wrong user and strand the profile just created. Only
the user is checked, not the host - iOS is never the controller.

Records the two hazards deliberately left as they are on master: the picker's unguarded
close(), which the ordinary row tap has had all along, and alertAfterDismissal's fixed
delay.
2026-08-07 12:10:11 +00:00
Narasimha-sc 80bcaf8405 ios: check for the ignored flag before refreshing the profile lists
Both iOS surfaces refreshed chatModel.users - and their own copy of it - immediately
after creating the profile, before checking whether the core had honoured
keepActiveUser. On the path where it did not, that list already has the new profile
marked active while chatModel.currentUser is still the previous one, so anything derived
from users disagrees with the active user until the resync lands. Kotlin checks first
and never opens that window.

Moving the check up also drops the refresh from that path entirely, since
changeActiveUserAsync_ does its own, and lets the remaining refresh share the
MainActor.run that was already there.
2026-08-06 20:31:03 +00:00
Narasimha-sc 51cb747d2a ios: derive the switch outcome from the active user instead of a flag
The one-time link picker's old-core fallback set selectedProfile to the new user
unconditionally, but the resync it does first is wrapped in a catch - so when the switch
threw, the picker put its checkmark on a profile that is not active and cannot be
selected, because the view then thinks it is already selected. Reading
chatModel.currentUser instead is right in both cases and is a line shorter.

The compose picker had the same question answered with a local switched flag. The active
user answers it there too, which removes the flag, the two assignments that set it, and
with them the mutable-local-in-a-@Sendable-closure problem that flag caused when it was
first introduced.
2026-08-06 19:15:56 +00:00
Narasimha-sc f20925e086 ios: trim the comment bulk
The Swift side had been minimised least: 57 of its added lines were comments, several
of them restating what the line below does or narrating what an earlier attempt got
wrong. Kept the ones that record a non-obvious why - why the sheet hangs off the Group,
why the check-and-set is atomic, why the resync is not rethrown, why the alert is
deferred - and cut the rest.

Also tried making changeProfile async so the create flow could await it and the
existing creatingProfile flag would cover the reassignment, removing changingProfile
entirely. It reads better but it takes changeProfile out of its Task and re-indents the
whole function, which costs more diff than the flag it saves. Not done.
2026-08-06 19:02:35 +00:00
Narasimha-sc 767db0af21 ios: disable the submit button while creating, as Kotlin already does
The form's only in-flight protection was the picker's own check-and-set, which makes a
second tap a silent no-op - the button stayed live with nothing visibly happening for
the whole of the create plus the profile list refresh. Kotlin gained a submitting flag
for this; iOS did not.

Also trims the last of the long doc comments on isLastModalOpenNotClosing.
2026-08-06 18:24:35 +00:00
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 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 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 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
EvgenyGitHubspaced4ndyEvgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
80538850f1 ui: show badges in more contexts (#7084)
* core: fix delivery cursor not advancing to maximum group member id for posgtgres (#7043)

* ui: show badge in user picker above message entry

* core: send badge with channel owner profile

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-06-17 19:10:37 +01:00
2dd54c6697 core, ui: prohibit changing profile for prepared entity when first attempt to connect failed (#6037)
* core: prohibit changing profile for prepared entity when first attempt to connect failed

* reuse incognito

* schema

* ios

* postgres schema

* ios

* reenable tests

* kotlin

* update alert

* rename predicate, combine queries

* send the correct incognito mode, fail on attempt to change mode for prepared connection

* query plans

* ui: show group connecting status

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2025-07-05 11:09:10 +01:00
EvgenyandGitHub 9794829d74 core: show group as sender, add contact card to contact request chats, unify api type for prepared and contact request chats (#6003)
* core: option to show group as sender of chat item (for the initial items, e.g. welcome message)

* add chat item to contact request chats

* return AChat for prepared chats and contact requests

* update iOS api types, show prepared contact as blue, show preview info when content message is not available (previously was showing feature item)

* ios: remove ContactType

* ios: show group as sender, fix avatar not showing when member message sequence starts with merged items

* ios: update compose UI for all connection scenarios

* address settings

* ios: address settings UI

* fix tests

* fix tests 2

* ios: fix minor issues
2025-06-23 14:42:00 +01:00
spaced4ndyandGitHub b0ee13628b core: change user for prepared contact or group (#5985) 2025-06-13 14:38:17 +00:00