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.
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.
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.
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.
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.
Two tests, both driving /_create user with JSON - the path the apps actually
use, which no test covered before:
- keepActiveUser creates the profile while leaving the active user alone, and
the prepared contact can then be reassigned to it and connected.
- omitting keepActiveUser still parses and still activates. This is a
regression guard: iOS does not send the field, so if the record ever stopped
accepting its absence, profile creation there would break silently.
Adds keepActiveUser to NewUser. When set, the new user is created but the
current active user is preserved.
This is needed to create a profile for an invitation: the prepared chat is
resolved under the active user by APIChangePreparedContactUser, so the profile
that owns the invitation has to stay active until the chat has been moved to
the new one. Creating and activating in one step makes that reassignment
impossible without switching back and forth.
BoolDef gives the field omittedField = False, so clients that do not send it -
iOS, the CLI, and any older caller - keep the current behaviour of activating
the new user.
keepActiveUser is ignored when there is no active user to keep, which would
otherwise leave the app with none at all.
Note the response is still CRActiveUser: it carries the created user, which is
not the active one on this path. Documented at the field.
Regenerates the API docs and the generated TypeScript and Python client types,
which are checked by the Bot API docs tests.
The generator emits BoolDef fields as required in the client types, so the two
hand-maintained clients that build a NewUser literal - simplex-chat-python and
simplex-chat-nodejs - stop type-checking until the field is added there too. No
test covers those; a4e3a1ea1 did the same when clientService was added.
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.
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.
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.
* 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>
* android, desktop: switch to the request's profile when accepting contact request from notification
* plans: justify accepting contact request from notification fix
* 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
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.
* 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>
Messages of blocked members are hidden, but they still marked chat item
as user mention, showing mention badge and counting group as unread in
"mentions only" notification mode:
- createNonLive passed mentions unfiltered, allowing members blocked by
admin to mention user - it is the default path, as it is only used
when full delete is not allowed;
- userReply was set for replies to user messages from blocked members.
* 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
* 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
* 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>
* 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`.