- remove all comments introduced by this PR across the touched files,
per review decision
- fix the "New Profile" row being hidden behind the bottom app bar in
one-handed UI mode once the profile list grows long enough to reach it
- stop force-setting connChatUsed on reassignment, so the "keep unused
invitation?" prompt correctly fires for a genuinely unused invitation
- fix NewChatView's contactConnection state going stale after a
reassignment (it never reflected the new connection), causing both
the keep/delete decision and any later picker reopen to reference an
already-superseded connection
- widen changingActiveUserMutex to cover currentUser/listUsers/users list
update atomically in changeActiveUser_
- avoid IndexOutOfBoundsException risk in the stale-core resync branch by
replacing indexed users-list mutation with map/clear/addAll
- make the resync retry cancellation-transparent and retry once on failure
- add a 60s watchdog so creatingProfileForInvitation can't get stuck true
- guard chatModel.showingInvitation updates against stale/cross-invitation
overwrites via a previousConnId check
- verify the switch actually landed (userId + activeUser flag) rather than
trusting an unthrown exception
- iOS: guard the retry-alert watchdog's dismiss so it never cascades onto
an unrelated alert stacked on top of it
- iOS: consolidate ContextProfilePickerView's two competing sheet booleans
into one Identifiable enum driving a single .sheet(item:)
* ui: show role (if exists) and remove "new" in open channel/group alerts
* restore removed strings
* info color
* docs
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* desktop: add bounded animated image decoder
Skia's Codec is already on the desktop classpath through skiko and decodes
both GIF and animated WebP. The frames come from a file somebody else
composed, so the decoder is bounded before it allocates: the raster is
measured in bytes with the sides multiplied as Long, each side is capped
separately so an extreme aspect ratio cannot slip under the byte budget, and
the encoded size is checked before the bytes are copied into native memory.
Anything outside the bounds, or any failure, keeps the still image the chat
already renders.
Nothing calls this yet.
* desktop: animate GIFs in chat items and full screen
Both views drew the first frame only. The full screen view also decoded its
still on every recomposition, which an animation recomposes once per frame,
so that decode is remembered against the data it comes from.
The chat list preview stays a still image: it is a 36dp box that the desktop
layout keeps on screen the whole time, so animating it would hold a raster and
spend a frame of work per listed chat, without pause.
Removes the two markers left for this work.
* desktop: don't decode animation frames that cannot be seen
With media blur on, a blurred image is only revealed while the mouse is over
it, so every frame was decoded, uploaded and then blurred away again for
nobody - and the blur is a render effect re-run per frame. Frames now decode
only while the image can be seen, which also stops motion showing through a
blur that is there to hide it.
Passing the blur state to the view is why the shared signature changes; coil
drives its own animation on Android, so there is nothing to pause there.
* docs: move animated images plan to plans/
* docs: drop file path references from animated images plan
* docs: correct animated images plan against the code
* desktop: correct animated image comments
* desktop: reduce animated image comments
* desktop: correct and bound animated image decoding
* docs: correct animated images plan against measurements
* desktop: fuse the animation prior frame decision
* docs: cover desktop animated images in spec and product
* desktop: drop the unused animated image component
* desktop: return the animation frame instead of its state
* docs: correct the animated images documentation
* desktop: don't decode animations under the full screen viewer
* desktop: bound the frames an animation rebuilds
* desktop: pause animations under any full screen modal
* desktop: stop animations that alternate expensive frames
* desktop: read what playing a frame needs only once
* desktop: close the codec of an animation outside the bounds
* desktop: wait out what an animation frame cost to decode
* docs: correct animated images claims against the code
* desktop: bound the frame count where the others are bounded
* desktop: don't wait out a stall an animation frame did not spend
* desktop: say what the slow frame constants stand for
* desktop: don't decode animations behind a minimised window
* desktop: make the animation frame wait testable
* desktop: bound the file size where the others are bounded
* desktop: pin the frame wait clamp in its test
* desktop: keep the frame wait clamp private
* desktop: reduce animated image comments
---------
Co-authored-by: sh <github.shum@liber.li>
* 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/
* 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% (2497 of 2497 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/
* 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 (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/
* Translated using Weblate (French)
Currently translated at 82.3% (2388 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/
* Translated using Weblate (French)
Currently translated at 92.6% (2314 of 2497 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* 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 (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/
* Translated using Weblate (French)
Currently translated at 83.3% (2416 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/
* Translated using Weblate (French)
Currently translated at 96.2% (2404 of 2497 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Added translation using Weblate (Slovak)
* Translated using Weblate (Slovak)
Currently translated at 5.0% (146 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/sk/
* Update translation files
Updated by "Remove blank strings" hook in Weblate.
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/
* Translated using Weblate (French)
Currently translated at 97.3% (2431 of 2497 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Danish)
Currently translated at 31.1% (903 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/da/
* Translated using Weblate (Slovak)
Currently translated at 14.8% (431 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/sk/
* Update translation files
Updated by "Remove blank strings" hook in Weblate.
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/
* Translated using Weblate (French)
Currently translated at 97.4% (2434 of 2497 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Slovak)
Currently translated at 16.2% (472 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/sk/
* Translated using Weblate (French)
Currently translated at 97.7% (2440 of 2497 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Danish)
Currently translated at 32.1% (932 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/da/
* Translated using Weblate (Slovak)
Currently translated at 18.1% (527 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/sk/
* Update translation files
Updated by "Remove blank strings" hook in Weblate.
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/
* 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% (2497 of 2497 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 98.7% (2466 of 2497 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Slovak)
Currently translated at 22.0% (638 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/sk/
* Update translation files
Updated by "Remove blank strings" hook in Weblate.
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/
* Translated using Weblate (French)
Currently translated at 83.3% (2416 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/
* Translated using Weblate (French)
Currently translated at 99.4% (2483 of 2497 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Slovak)
Currently translated at 22.3% (647 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/sk/
* Translated using Weblate (Portuguese (Brazil))
Currently translated at 82.9% (2405 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/
* Translated using Weblate (Slovak)
Currently translated at 24.1% (700 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/sk/
* Translated using Weblate (Portuguese (Brazil))
Currently translated at 83.1% (2411 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/
* Translated using Weblate (French)
Currently translated at 85.7% (2486 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/
* Translated using Weblate (French)
Currently translated at 99.7% (2491 of 2497 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Portuguese (Brazil))
Currently translated at 92.3% (2678 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/
* Translated using Weblate (French)
Currently translated at 99.7% (2491 of 2497 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.2% (2791 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/
* Translated using Weblate (Slovak)
Currently translated at 26.0% (754 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/sk/
* Update translation files
Updated by "Remove blank strings" hook in Weblate.
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/
* Translated using Weblate (French)
Currently translated at 86.6% (2512 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/
* Translated using Weblate (French)
Currently translated at 99.7% (2492 of 2497 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.3% (2794 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/
* Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.8% (2809 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/
* Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.9% (2811 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/
* Translated using Weblate (Slovak)
Currently translated at 26.9% (781 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/sk/
* Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.7% (2833 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/
* Translated using Weblate (Portuguese (Brazil))
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/pt_BR/
* Translated using Weblate (Portuguese (Brazil))
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/pt_BR/
* Translated using Weblate (French)
Currently translated at 88.8% (2576 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/
* Translated using Weblate (French)
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/fr/
* Translated using Weblate (Portuguese (Brazil))
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/pt_BR/
* Translated using Weblate (Portuguese (Brazil))
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/pt_BR/
* Translated using Weblate (Portuguese (Brazil))
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/pt_BR/
* Translated using Weblate (Slovak)
Currently translated at 26.9% (781 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/sk/
* Translated using Weblate (French)
Currently translated at 90.4% (2622 of 2899 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/
* Translated using Weblate (Portuguese (Brazil))
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/pt_BR/
* ios: bump Xcode version in localization exports
* process translations
* ui: fix translation errors in fr, pt-BR, sk, da and de
Review of the incoming Weblate batch found no malicious or deceptive
translations: every URL, e-mail and format specifier matches the English
source, and there are no bidi overrides, zero-width characters or
homoglyphs. These are accuracy fixes.
Security/privacy wording:
- fr (iOS): "no E2E encryption" was rendered "chiffrement pair-à-pair"
(peer-to-peer). The same file translates "peer-to-peer" that way, so
the E2E warning for chats with admins in public channels read as the
wrong guarantee.
- pt-BR: "We don't store any of your contacts or messages (once
delivered)" lost the first-person commitment and applied "once
delivered" to contacts too; "your profile will be shared via the
address" became a capability rather than a consequence; the
"(your contact can mark them for deletion)" parenthetical was dropped
from both irreversible-deletion descriptions.
- fr + pt-BR: in why_built_p7 the anaphora ("ce pouvoir" / "esse poder")
resolved to the power to identify you rather than the reader's own
power, inverting the sentence. de, it, es, ru, sk and hu all get this
right.
Broken output:
- da: proxy_destination_error_unknown_ca was wrapped in literal quotes
(Android's whitespace-quoting syntax) and carried a double space.
- fr (NSE): "De %d conversations(s)".
- pt-BR: incognito_info_share had lost every diacritic; mark_unread was
indicative instead of imperative.
Terminology and grammar:
- fr: the chat engine was translated as "conversation" on iOS (Start
chat, Stop chat, Stopping chat, Chat migrated, and the multi-device
delivery-failure warning) while Android used "messagerie"; "report"
(a moderation report) was translated as "rapport"; empty-list strings
read "Aucunes conversations".
- pt-BR: "timeout" was dropped from two duration settings; "Proxied
servers", "Reachable toolbar", "Allow downgrade" and "Proxy
authentication" had drifted.
- sk (new locale): ~120 fixes - typos and missing diacritics, Czech
forms (databázi, události, není, jednorázový, language names), gender
and number agreement, and 18 mistranslations including "encryption
agreed" rendered as "allowed", "Non-profit governance" as "vláda"
(government), "embed" as "vsadiť" (to wager) and "Decode link" as a
noun phrase.
- da: "Upgrader" -> "Opgrader", and name resolution described with
"afkode" (decode).
- de: "Privacy was never a feature" had gained a "nur".
Verified after the change: XML well-formed, no duplicate/orphan/removed
keys, format specifiers, markup, URLs and newline counts all match the
English source, iOS .strings and .xliff stay in sync, and no unescaped
apostrophes were introduced.
---------
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: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Ranakan <ranakan.dev@protonmail.ch>
Co-authored-by: summoner001 <summoner@disroot.org>
Co-authored-by: ND <nd@users.noreply.hosted.weblate.org>
Co-authored-by: Martin Kozempel <rado2012mir@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Morten Juhl-Johansen <git@0b.dk>
Co-authored-by: SanctumSaturn <oraculogithub@gmail.com>
Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com>
* desktop: fix stretched video preview and playback for AV1 videos
libvlc passes the padded size the decoder allocated to the buffer format
callback, not the size of the picture. dav1d pads to a multiple of 128, so
a 1920x1080 AV1 video arrives as 1920x1152, and vlc scales the picture to
fill it - the preview sent with the message, and desktop playback, were
6.7% too tall. H264 pads much less, so it was barely visible there.
Ask for the size of the track being played instead. It is already populated
when the buffer format is negotiated, and matching the track that is playing
matters for files with more than one video track, where the first track is
not necessarily the one being decoded. Falls back to the previous behaviour
when the track is not known.
* plans: desktop video preview aspect ratio
* android, desktop: open group member profile without loading all members
Clicking member avatar in chat loaded the whole member list (apiListMembers)
before showing member profile, and it was repeated on every click - in a group
with 10000 members it takes several seconds.
The full list is not needed to show the profile of one member, so instead the
opened member is added to the model, the same way as in iOS app.
* plans: member profile in large groups
* plans: correct relay warning section - it is not affected by the change
* core, ui: auto-accept group invitations per user profile
Add a per-profile toggle for auto-accepting group invitations, and regroup it
with the existing contact-requests setting under a single Auto-accept section
in Privacy & Security, relabelled "Contact requests in groups".
The join is fully async. processGroupInvitation already had an async accept
path, used when the invitation matches a group link the user opened:
prepareAgentJoin + createMemberConnectionAsync + joinAgentConnectionAsync,
with the outcome reported later against the CFJoinConn command id. Auto-accept
takes that same path instead of going through APIJoinGroup, so it works while
the app is closed and never blocks message processing.
An auto-accepted invitation still records a CIRcvGroupInvitation item in the
chat with the inviting contact, so there is a record of who added the user to
which group.
Two details worth noting for review:
hostContact is reported to clients only for group links. Clients respond to it
by replacing the transient host connection view with the group and removing
that chat - correct for a group link, where the contact is a placeholder, but
wrong for a plain invitation, where it is a real contact.
A resent invitation returns the existing group, because createGroupInvitation
is idempotent on inv_queue_info. The join therefore only runs while the
membership is still GSMemInvited, so a resend cannot open a second connection.
* booldef
* order
* refactor
* update translation key
* query plans
* ios: export translations
---------
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
* ui: send dropped .webm as video only when it has a video track
.webm is as commonly an audio-only container as a video one, so the
extension alone cannot tell whether there is a frame to embed. Read the
container to decide: files with a video track are sent as video, the rest
as files.
Only done for files attached without the user saying how to send them
(drag & drop, paste). An explicitly picked video is still sent as one, so
.webm is now listed in the video file picker too.
* plan: send dropped .webm as video only when it has a video track
* android: add google and foss build flavors
* android: get the Google Play account country in the google flavor
* android, desktop: update whats new
* temp: comment out wefunder link
* ui: connect when a SimpleX link is pasted as a markdown hyperlink
* remove comments
Co-authored-by: Evgeny <evgeny@poberezkin.com>
---------
Co-authored-by: Evgeny <evgeny@poberezkin.com>
changeActiveUser_ writes currentUser inside its mutex before it lists users,
and changeActiveUserAsync_ runs both API calls before the MainActor.run that
assigns m.users - so a throw in between leaves the model pointing at a
profile its own list does not contain. The comment claiming the resync
covers it was wrong on exactly that path.
Also rethrow CancellationException rather than reporting it as a failure,
hop to main before showAlert on the immediate path (it runs inside a Task,
and only alertAfterDismissal was hopping), and put the new row's divider
under it - reverseLayout flips items, not the content of one, so it was
drawing a line along the picker's top edge.
clear() + addAll() ran from Main while changeActiveUser_ does the same pair
from withBGApi, so a notification action switching user mid-creation could
interleave them and leave chatModel.users duplicated or short. The refresh
had nothing to add over the row already appended - newUser is the API's own
record and a new profile has no unread messages - so it is gone.
changeProfile is reached from the create flow and from a plain row tap;
routing both through alertAfterDismissal delayed row-tap errors half a second
for no sheet, detaching them from the tap. Pass dismissingSheet instead.
Also gate the remaining ungated incognito write, matching its sibling and
both Kotlin ones.
The form being covered by another modal was the only branch here that said
nothing: the profile exists, the invitation did not move, and the form comes
back with the name still typed so Create fails as a duplicate. Add
isModalOpenNotClosing to tell covered from dismissed - excluding toRemove, or
the close animation would report a dismissal as a cover.
Gate the compose picker's branches rather than its rows. Master had no gate
there at all, so blocking the local expand/collapse was a regression this
branch introduced; only changeProfile and the incognito writes need it.
Fixing only apiCreateActiveUser's anchor left the table self-contradictory,
with listUsers pointing above it despite coming after. The whole 2.1 section
was stale by ~30 lines; it is now monotonic and matches the file.
Also record why the 'the failed move is silent' finding, raised three times,
does not hold: offline gets the retry dialog, every other error alerts, and
only the user cancelling their own dialog is quiet.
Above the stale-host branch it left two entries flagged activeUser when an
old core activated the profile anyway, so a failed resync made
firstOrNull { it.activeUser } resolve to whichever came first.
It also only ever updated chatModel.users, never the pickers' own users and
profiles state, which is otherwise filled in onAppear alone - so on the
ownership-guard path the profile existed everywhere except the picker the
user was looking at. Both lists now update at one point, which replaces the
later fallbacks.
enableClose = false removes ModalView's handler rather than neutering it,
so Compose passed Back down to ChatView's, closing the chat behind the form
that was still on screen. Register a no-op handler that wins while creating.
Also guard the iOS incognito row on contactConnection, as Kotlin already
does: incognitoEnabled is a binding onto the app-wide default, so starting
a change that cannot happen wrote that preference twice to undo itself.
ModalView's Back handler and back arrow were live while the profile was
being created, so Back/Esc closed the form between creation and the
reassignment: profile created, invitation never moved, nothing said, and
the next attempt at the same name fails as a duplicate. Gate them on the
in-flight flag, as MigrateFromDevice and ChooseServerOperators do, which
is the Kotlin equivalent of iOS's interactiveDismissDisabled.
Move the chatModel.users registration above the stale-host branch, matching
what iOS got in 54e2fac3d: changeActiveUser_ throws before its own refresh,
so the block below the early return never ran.
Also fix the incognito twin of a bug this branch already fixes on iOS: with
no connection to change, profileSwitchStatus stayed .switchingIncognito and
left the whole picker dead. Pre-existing, but immediate now that hit testing
keys on the status rather than the delayed spinner flag.
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.
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.
The refresh is best-effort, and nothing else adds the profile to
chatModel.users - so a failed listUsers left the picker, which is keyed on
users.size, with no row for the profile just created, and creating it again
fails on the duplicate name. iOS already had this fallback.
Also record in the plan the four review findings left unfixed, with why.
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.
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.
* android, desktop: fix live message sent to the chat opened after switching
A live message is sent when the chat is switched, but by then this view
already shows the chat that was opened - the effect that sends it runs
with that chat, so the message typed in one chat was sent to another.
The chat the message was composed in is passed to the send, and it is
resolved by the chat id from before the switch. If that chat is no longer
there the message is not sent at all, rather than sent to the chat opened
instead. The draft cleared after sending is the one of that chat too.
* plan: correct references; clear the draft of the chat the message is sent to
* plan: note the blast radius and how to resolve the overlap with #7308
* android, desktop: only pass the chat to what a live message can reach
A live message has no context item, so the forwarding, editing and
reporting branches of the send cannot run for it - they keep using the
chat of the view, and the chat it was composed in is passed only to the
message send, to the update of an already sent live message, and to
clearing the draft after sending.
* android, desktop: give the opened chat its own compose state while the live message is sent
Sending the live message to the chat it was composed in is not enough on
its own: composeState is shared between the chats opened in this view, and
the chat switch branch of KeyChangeEffect is the only one that neither
resets it nor loads the opened chat's draft - the branch that loads a
draft is later in the same if chain and cannot be reached.
sendMessageAsync then made it visible. It runs on Dispatchers.Default, so
its writes land after the switch: the whole composed state (via
cs.copy(liveMessage = null)) and its spinner (sending()) were written to
the compose state of a view that already shows another chat, which then
displayed the text composed in the previous one until the send completed.
The draft it should have shown was still in the model, and the next switch
away dropped it.
- sendMessageAsync takes composed, and sendMessage takes it as null by
default, so only the chat switch passes a state and every other sender
reads it inside the coroutine, where the send read it before. The chat
switch captures it on the main thread before replacing it - otherwise
the send would read the compose state of the chat that was opened and
send its draft to the previous chat.
- checkLinkPreview takes that state too. It re-read composeState rather
than what was passed in, and every text live message reaches it through
updateMsgContent, so it would have rebuilt the message from the opened
chat's draft instead of committing what was composed.
- every composeState write in sendMessageAsync is guarded by
composeIsForSend() (toChat.id == chat.id), which compares the two chats
instead of checking which one is open, so this send never takes the
compose state back if that chat is opened again before it completes.
- the chat switch branch then resets composeState to the opened chat's
draft, or to an empty state, like the branches below it do.
* plan: document the compose state handoff; correct the #7308 overlap resolution
The note on resolving the overlap with #7308 said its cs.liveMessage !=
null clause "already covers the send made by the chat switch". It does
not - in #7308 that clause sits outside the chatIsOpen check, which is
correct only while a live message is always sent to the chat that is
open, the assumption this fix removes. Read as written it exempts the
chat switch send from the guard that protects the opened chat, and a
merge that follows it reintroduces the leak.
Also records what manual test 2 was found failing on, and adds a slow
send variant so the window between the switch and the send completing is
long enough to type in the chat that was opened.
* android, desktop: keep reading the current state where a forward appends it
checkLinkPreview taking the composed state is needed where a live message
reaches it, but the forwarding branch is not one of those - it cannot run
with a chat other than the view's - and forwardItem suspends before it. So
there the captured state is stale by a network round trip, and text typed
while the forward was in flight stopped being appended to the message it
adds, while still being cleared when the send completed.
* plan: correct references and the claims that no longer hold
Line references were against the base this branch forked from, before
#7308 landed. Also: the live message loop no longer exits because the send
clears liveMessage - the chat switch replaces the compose state, on the
main thread, before the send runs; checkLinkPreview is not passed the
captured state everywhere; and chatsCtx.getChat can only return null in a
secondary context, which is not how "the chat is gone" reads.
Adds the two manual checks the review implied: a live message carrying a
link preview, which is what breaks if checkLinkPreview stops reading the
state it was given, and returning to the chat before the send completes.
* android, desktop: narrow the change to what the fix needs
- the draft id a failed message is saved under keeps using chat: that
branch is behind !liveSend, which the send made by a chat switch never
satisfies, so toChat is always chat where it is read;
- the state the chat switch installs no longer carries maxFileSize over.
That field is kept in sync on chat switch by LaunchedEffect(chat.chatInfo),
which is why the branches below this one construct it without one, and
the paths where that effect does not re-run are the ones where the send
overwrites the state anyway;
- sendMessageAsync takes its cs as a parameter rather than aliasing a
separately named one.
* plan: follow the narrowed change
* android, desktop: shorten the comments
The threading mechanism behind cs is explained where it is used, so the
function comment only has to say what it is; the rest is rewording.
showProgress existed to skip the picker's 500ms grace while a profile was being created,
because the recomposition that follows the form closing restarts that timer. But the rows
are disabled by busy the whole time, so nothing incorrect can be done - the gap is purely
that the spinner appears late, and a 500ms grace before showing it is what this picker
already chose for every other switch. Same for the two alpha modifiers added to the
compose picker, which had no progress indication before this branch and does not need to
gain one here.
appPreferences.incognito.set(false) goes back where it was. Moving it after the
connection change fixes a real failure-path bug - a reassignment that fails no longer
clears the app-wide default - but that bug is master's and is reachable from the ordinary
row tap, which this branch does not otherwise touch.
Kept the profileChangeProhibited guard on the new row even though ProfilePicker only
renders when the flag is false: it is live state the receiver loop flips, so it can turn
true between the row being laid out and the tap landing, and five lines to avoid creating
an orphan profile in that window is worth it.
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.
CreateProfile took a required onSubmit, so both existing call sites had to change and a
createProfileFromForm helper existed only to give them somewhere to delegate. The iOS
port of the same change made the callback optional and touched no existing call site;
this does the same. UserPicker and UserProfilesView drop out of the diff, the helper
goes, and the branch the form always had stays where it was, behind a null check.
The trade is that chatModel and close stay parameters even though the invitation path
uses neither, and the callback is nullable. That is the price of not touching two
unrelated files, and it is the shape iOS already ships. Easy to reverse if the cleaner
signature is preferred.
Three changes that fixed pre-existing bugs the feature neither introduces nor makes
materially more reachable. Each is defensible on its own and none belongs here.
The one-time link picker's trailing close() popped whatever was on top rather than
itself, so backing out during the reassignment dismissed the New chat screen too. The
window it needs is the connection change plus the user switch - which the ordinary row
tap already has, unchanged, on master. Creating a profile first is local and fast and
adds almost nothing to it. Reverted, which takes ModalViewId.ACTIVE_PROFILE_PICKER and
ShareListView out of the diff entirely.
Same on iOS for allowsHitTesting: blocking the whole picker from t=0 instead of t=0.5s
is a change to the existing switch flow, and the two else branches releasing
profileSwitchStatus only matter when there is no connection - a state this feature
gates itself out of. The add-profile row keeps its own disabled state, which is what
the feature actually needs.
changingProfile in the compose picker was the same thing again: the feature's own
window is already covered by ChatModel.creatingProfileForInvitation, which is held
across the reassignment, so this flag only guarded plain row taps. Dropping it also
removes the try/finally that existed solely to reset it.
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.
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.
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.
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.