Compare commits

..

69 Commits

Author SHA1 Message Date
spaced4ndy e67feeea22 operators: debug decoding 2024-11-12 11:02:58 +04:00
Evgeny Poberezkin da65474452 CLI API in progress, validateUserServers 2024-11-11 23:24:11 +00:00
Evgeny Poberezkin d0a7e14a96 make preset servers lists potentially empty in some operators, as long as the combined list is not empty 2024-11-11 16:22:19 +00:00
Evgeny Poberezkin bd4745775d update 2024-11-11 11:34:02 +00:00
Evgeny Poberezkin af144c6208 fix 2024-11-10 22:58:23 +00:00
Evgeny Poberezkin 74206a947b Merge branch 'server-operators' into ep/operators-preset-servers 2024-11-10 16:15:39 +00:00
Evgeny Poberezkin 457e12880c Merge branch 'master' into server-operators 2024-11-10 16:15:07 +00:00
Evgeny 2d588949b1 directory service: additional commands (#5159)
* directory service: additional commands

* notify superusers

* 48 hours

* replace T.elem
2024-11-10 15:21:33 +00:00
Evgeny Poberezkin 84f7f901ea remove imports 2024-11-10 12:57:57 +00:00
Evgeny Poberezkin 90ed503ee0 update (most tests pass) 2024-11-10 12:30:24 +00:00
Evgeny Poberezkin 28105038d4 preset operators and servers (compiles) 2024-11-09 23:53:18 +00:00
Evgeny Poberezkin fd60a2402a Merge branch 'server-operators' into ep/operators-preset-servers 2024-11-08 14:36:47 +00:00
spaced4ndy ef0f21a11c core: operator apis commands (#5155) 2024-11-08 14:45:00 +04:00
spaced4ndy d0e40c1f1d Merge branch 'server-operators' into ep/operators-preset-servers 2024-11-06 16:20:03 +04:00
spaced4ndy 8396e70e7b core: validate servers - find servers with duplicate hosts (#5150) 2024-11-06 16:13:08 +04:00
Evgeny Poberezkin 6128a24869 database entity IDs 2024-11-06 08:12:16 +00:00
Evgeny Poberezkin 0329a6a7d3 Merge branch 'server-operators' into ep/operators-preset-servers 2024-11-05 18:07:09 +00:00
spaced4ndy 2da89c2cf1 core: setConditionsNotified, acceptConditions, setUserServers, validateServers apis wip (#5147) 2024-11-05 21:40:33 +04:00
Evgeny Poberezkin 601ddf97ce WIP 2024-11-05 16:29:30 +00:00
Evgeny Poberezkin 128d031ced Merge branch 'server-operators' into ep/operators-preset-servers 2024-11-05 10:55:36 +00:00
spaced4ndy 3b0205b25f core: setServerOperators, getUsageConditions api wip (#5145) 2024-11-05 14:15:20 +04:00
Evgeny Poberezkin d4a47f1cce simplify 2024-11-04 20:07:28 +00:00
Evgeny Poberezkin 2998e3af3f usageConditionsToAdd 2024-11-04 19:44:45 +00:00
Evgeny Poberezkin 5ff838d63e Merge branch 'server-operators' into ep/operators-preset-servers 2024-11-04 18:19:24 +00:00
spaced4ndy bdaec30fa0 core: getServerOperators, getUserServers, getUsageConditions apis wip (#5141) 2024-11-04 21:11:03 +04:00
Evgeny Poberezkin 5a8bf9106e core: preset servers and operators (WIP) 2024-11-04 16:42:37 +00:00
Evgeny 97df069730 core: add support for server operators (#4961)
* core: add support for server operators

* migration

* update schema and queries, rfc

* add usage conditions tables

* core: server operators new apis draft

* update

* conditions

* update

* add get conditions api

* add get conditions API

* WIP

* compiles

* fix schema

* core: ui logic in types (#5139)

* update

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-11-04 13:28:57 +00:00
Evgeny Poberezkin 9a1166f097 Merge branch 'master' into server-operators 2024-11-03 09:12:12 +00:00
Evgeny Poberezkin 7a741e7ac4 ios: update core library 2024-11-02 20:03:27 +00:00
Alexander Bondarenko 165143a111 Use simplexmq with client_library flag (#5133)
* Use simplexmq with client_library flag

* fix server config for mq master

* simplexmq

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-11-02 17:51:11 +00:00
Evgeny ceb17b23b4 bumped haskell.nix (#5134)
Co-authored-by: Moritz Angermann <moritz.angermann@gmail.com>
2024-11-02 15:28:41 +00:00
Evgeny Poberezkin 3c8c9d8b52 website: update jobs page 2024-11-02 13:43:45 +00:00
Stanislav Dmitrenko 4162bccc46 multiplatform: edge to edge design (#5051)
* multiplatform: insets

* more features and better performance

* calls and removed unused code

* changes

* removed logs

* status and nav bar colors

* chatList and newChatSheet search fields

* overhaul

* search fields, devtools, chatlist, newchatsheet, onehand on desktop, scrollbars

* android, desktop: update to Compose 1.7.0

- support image drag-and-drop from other applications right to a chat
(with and without transparent pixels - will be png or jpg)

* stable

* workaround

* changes

* ideal adapting height layout

* dropdownmenu, userpicker, onehandui, call layout, columns

* rename bars properties and strings

* faster update and better layout

* gallery in landscape with cutout

* better cutout

* 1% step on slider

* app bar moves to bottom in one hand ui

* default alpha

* changes

* userpicker colors

* changes

* blur

* fix wrong drawing area in chatview

* fix

* fixed differently

* changes

* changes

* android fix

* Revert "android fix"

This reverts commit 7d417afd9b.

* changes

* changes

* blur

* swap

* no logs

* fix build

* old Android support

* fix position of menu

* disable blur on Android 12

* call button padding

* useless code

* fix padding in group info view

* rename

* rename

* newline

* one more fix

* changes

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-31 17:26:17 +00:00
Stanislav Dmitrenko 24090fe350 android, desktop: update to Compose 1.7.0 (#5038)
* docs: correction

* android, desktop: update to Compose 1.7.0

- support image drag-and-drop from other applications right to a chat
(with and without transparent pixels - will be png or jpg)

* stable

* workaround

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-31 17:11:26 +00:00
spaced4ndy 37b78edb91 ios: move Network and servers settings modules to folder (#5110) 2024-10-28 18:18:26 +04:00
spaced4ndy 78510b6fd3 core, ios: get messages for multiple last notifications; separately get notification connections before requesting messages (to avoid acknowledgement races in case of parralel nse threads); coordinate nse threads (#5084)
* core, ios: get messages for multiple last notifications (#5047)

* ios: refactor notification service (#5086)

* core, ios: separately get notification connections before requesting messages; coordinate nse threads (#5085)
2024-10-25 20:09:59 +04:00
Evgeny Poberezkin edf99fcd1d 6.1.1: ios 245, android 249, desktop 74 2024-10-18 18:37:14 +01:00
spaced4ndy 2ffabd1ef8 ios: fix changing user via notification (#5069) 2024-10-18 18:07:38 +04:00
Evgeny Poberezkin 28383edb83 core: 6.1.1.0 (simplexmq: 6.1.1.0) 2024-10-18 14:21:17 +01:00
Evgeny 9175897acf core, ui: add SMP STORE error (#5071)
* core, ui: add SMP STORE error

* update library
2024-10-18 14:17:04 +01:00
Evgeny Poberezkin f3cd167502 core: ntf server 2024-10-18 13:06:47 +01:00
Arturs Krumins 7cde2cf6c2 ios: optimise ComposeView rendering (#5042)
* ios: replace revealed bindings with constant value

* ios: optimise ComposeView rendering

* rename

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-18 12:35:27 +01:00
Arturs Krumins 3913043705 ios: fix chat not loading if initial page has too many merged items (#5066)
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-18 10:07:18 +01:00
Arturs Krumins a160acef12 ios: fix navigation title redaction after biometric authentication (#5065) 2024-10-18 10:04:53 +01:00
Arturs Krumins c54fae0136 ios: fix sheets dismissing during biometric authentication (#5062)
* ios: fix sheets dismissing during biometric authentication

* remove AppSheet

* Revert "remove AppSheet"

This reverts commit 3aa1688cbd.

* remove local auth request on sheet dismissal

* revert biometricAuth
2024-10-16 19:55:59 +01:00
Arturs Krumins d57abfcc93 ios: fix theme import file picker (#5048)
* ios: fix theme import file picker

* minor
2024-10-16 19:48:13 +01:00
Evgeny 515a0ddfdd blog: wired's attack on privacy (#5063)
* blog: wired misleading attack on privacy of communications

* image

* update

* title

* update

* update

* preview
2024-10-16 19:25:47 +01:00
spaced4ndy b5d8c65249 ui: quota error description (#5037) 2024-10-15 12:01:06 +04:00
Arturs Krumins de94892fe7 ios: replace revealed bindings with constant value (#5027) 2024-10-15 08:58:54 +01:00
Evgeny Poberezkin b7131e16f2 docs: fix links 2024-10-14 13:27:04 +01:00
Evgeny 11a44dc1fd blog: v6.1 and security review announcement (#5040)
* blog: v6.1 and security review announcement

* update, images

* readme

* update review links on home page

* links to review
2024-10-14 13:18:48 +01:00
sh 0af718f03f flatpak: update metainfo (#5039) 2024-10-14 10:12:00 +01:00
Evgeny Poberezkin f8f5c3c6be docs: correction 2024-10-14 09:37:31 +01:00
sh 13912a4af9 docs/smp-server: update to latest changes (#4960)
* docs/smp-server: update to latest changes

* update

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-13 09:43:47 +01:00
Evgeny Poberezkin 601b4cd619 6.1: ios 244, android 247, desktop 73 2024-10-12 12:08:16 +01:00
Evgeny d2b4b7bed6 ui: translations (#5031)
* Translated using Weblate (Spanish)

Currently translated at 100.0% (2089 of 2089 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% (1843 of 1843 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1843 of 1843 strings)

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

* Translated using Weblate (Czech)

Currently translated at 96.1% (2008 of 2089 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1843 of 1843 strings)

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

* process localizations

---------

Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: summoner001 <summoner@vivaldi.net>
2024-10-12 11:36:49 +01:00
Diogo fa95e4e9ad ios: dont show tails for moderated and blocked items unless revealed (#5030)
* ios: stop showing tails for non revealed moderated or blocked items

* simplify

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-12 10:59:51 +01:00
Evgeny Poberezkin 88fdc1ef75 core: 6.1.0.9 2024-10-12 10:56:24 +01:00
Evgeny 7ab6e44a6e directory service: list pending groups (#5029)
* directory service: list pending groups

* user commands to remove a group from directory and to set initial member role (TODO tests)

* tests
2024-10-12 10:33:45 +01:00
Evgeny Poberezkin 26986686ca ios: fix link previews to be enabled by default 2024-10-12 09:06:05 +01:00
spaced4ndy e76dc33cf0 core: associate new contact with all corresponding members on member contact re-creation (e.g. after it was merged to many members and then deleted) (#5028) 2024-10-11 20:47:54 +01:00
Evgeny aa2eafdacb blog: v6.1 announcement placeholder (#5004)
* blog: v6.1 announcement placeholder

* draft
2024-10-11 20:46:08 +01:00
Evgeny Poberezkin 2c3c97f5cc 6.1-beta.5: ios 243, android 246, desktop 72 2024-10-11 18:46:45 +01:00
Evgeny 83f42704ea ui: translations (#5026)
* Translated using Weblate (Italian)

Currently translated at 100.0% (1843 of 1843 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (2089 of 2089 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% (1843 of 1843 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (1843 of 1843 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1843 of 1843 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1843 of 1843 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (German)

Currently translated at 99.7% (1839 of 1843 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1843 of 1843 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1843 of 1843 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1843 of 1843 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 65.1% (1201 of 1843 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1843 of 1843 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (1843 of 1843 strings)

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

* process localizations

---------

Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: Abdullah Koyuncu <wisewebworks@outlook.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: acevif <acevif@gmail.com>
2024-10-11 17:08:32 +01:00
Stanislav Dmitrenko 7ff6ef09fe android: hide mic icon when mic is disabled (#5025) 2024-10-11 16:54:15 +01:00
Evgeny Poberezkin dbe4504f05 core: 6.1.0.8 (simplexmq: 6.1.0.7) 2024-10-11 15:44:13 +01:00
spaced4ndy 9a87f344b5 core: do not regenerate key when accepting connection to avoid invalidating invitation link on bad networks (#5018)
* core: prepare conn (plan)

* update

* group join

* comment

* comment

* wip

* Revert "wip"

This reverts commit 0849f43377.

* accept

* save contact_id, reuse contact

* refactor

* simplexmq

* set contactUsed

* support retrying join

* exclude prepared connections from API responses

* avoid race with events

* avoid race better

* fix UI

* update library

* tmp

* update

* display error details on ios cmd prohibited

* underscore instead of empty

* Update apps/ios/Shared/Model/SimpleXAPI.swift

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* test

* update simplexmq

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Diogo <diogofncunha@gmail.com>
2024-10-11 15:37:38 +01:00
Stanislav Dmitrenko 2127c7dcce android, desktop: don't stop audio track on Android in calls (#5024)
* android, desktop: don't stop audio track on Android in calls

There is a problem related to managing selected audio output device in
call. When microphone is disabled, WebView turns speaker on without
need. No way to prevent it was found yet. This is temporary workaround
that makes everything work except it makes microphone icon visible in
status bar (microphone is not used actually in that moment)

* enabled=false
2024-10-11 14:37:36 +01:00
Stanislav Dmitrenko e3528d3ffe android: re-apply custom language when webview appears (#5022)
* android: re-apply custom language when webview appears

There is a bug on Android related to including WebView. App language
changes to system language regardless of what was set before in
context's configuration. Re-set needed to be done twice: after creating
of WebView and after removing it from a view

* add link to bug

---------

Co-authored-by: Evgeny <evgeny@poberezkin.com>
2024-10-11 14:36:57 +01:00
262 changed files with 7480 additions and 3321 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local
# Compile simplex-chat # Compile simplex-chat
RUN cabal update RUN cabal update
RUN cabal build exe:simplex-chat RUN cabal build exe:simplex-chat --constraint 'simplexmq +client_library'
# Strip the binary from debug symbols to reduce size # Strip the binary from debug symbols to reduce size
RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \ RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \
+4 -2
View File
@@ -25,7 +25,9 @@ While SimpleX Chat Ltd is not a communication service provider, and provide publ
We see users and data sovereignty, and device and provider portability as critically important properties for any communication system. We see users and data sovereignty, and device and provider portability as critically important properties for any communication system.
SimpleX Chat security assessment was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 see [the announcement](/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). The implementation security assessment of SimpleX cryptography and networking was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 see [the announcement](/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
The cryptographic review of SimpleX protocols design was done in July 2024 by Trail of Bits see [the announcement](/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md).
### Your information ### Your information
@@ -172,4 +174,4 @@ You accept the Conditions of Use of Software and Infrastructure ("Conditions") b
**Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd at any time by deleting our Applications from your devices and discontinuing use of our Infrastructure. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd. **Ending these conditions**. You may end these Conditions with SimpleX Chat Ltd at any time by deleting our Applications from your devices and discontinuing use of our Infrastructure. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the conditions, Enforcing the conditions, and Ending these conditions will survive termination of your relationship with SimpleX Chat Ltd.
Updated April 24, 2024 Updated October 14, 2024
+5 -9
View File
@@ -233,14 +233,12 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent and important updates: Recent and important updates:
[Oct 14, 2024. SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md)
[Aug 14, 2024. SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md) [Aug 14, 2024. SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md)
[Jun 4, 2024. SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) [Jun 4, 2024. SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md)
[Apr 26, 2024. SimpleX network: legally binding transparency, v5.7 released with better calls and messages.](./blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md)
[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) [Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md)
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md) [Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
@@ -249,10 +247,6 @@ Recent and important updates:
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md). [Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md).
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md).
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md). [Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
[Mar 1, 2023. SimpleX File Transfer Protocol send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md). [Mar 1, 2023. SimpleX File Transfer Protocol send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md).
@@ -409,7 +403,9 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
[SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed, and had many breaking changes and improvements in v1.0.0. [SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed, and had many breaking changes and improvements in v1.0.0.
The security audit was performed in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2.0 see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). The implementation security assessment of SimpleX cryptography and networking was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about) see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
The cryptographic review of SimpleX protocols was done in July 2024 by Trail of Bits see [the announcement](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md).
SimpleX Chat is still a relatively early stage platform (the mobile apps were released in March 2022), so you may discover some bugs and missing features. We would really appreciate if you let us know anything that needs to be fixed or improved. SimpleX Chat is still a relatively early stage platform (the mobile apps were released in March 2022), so you may discover some bugs and missing features. We would really appreciate if you let us know anything that needs to be fixed or improved.
+3 -1
View File
@@ -13,6 +13,7 @@ struct ContentView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@ObservedObject var alertManager = AlertManager.shared @ObservedObject var alertManager = AlertManager.shared
@ObservedObject var callController = CallController.shared @ObservedObject var callController = CallController.shared
@ObservedObject var appSheetState = AppSheetState.shared
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@EnvironmentObject var sceneDelegate: SceneDelegate @EnvironmentObject var sceneDelegate: SceneDelegate
@@ -250,7 +251,8 @@ struct ContentView: View {
private func mainView() -> some View { private func mainView() -> some View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
ChatListView(activeUserPickerSheet: $chatListUserPickerSheet).privacySensitive(protectScreen) ChatListView(activeUserPickerSheet: $chatListUserPickerSheet)
.redacted(reason: appSheetState.redactionReasons(protectScreen))
.onAppear { .onAppear {
requestNtfAuthorization() requestNtfAuthorization()
// Local Authentication notice is to be shown on next start after onboarding is complete // Local Authentication notice is to be shown on next start after onboarding is complete
+1
View File
@@ -147,6 +147,7 @@ final class ChatModel: ObservableObject {
@Published var chatDbEncrypted: Bool? @Published var chatDbEncrypted: Bool?
@Published var chatDbStatus: DBMigrationResult? @Published var chatDbStatus: DBMigrationResult?
@Published var ctrlInitInProgress: Bool = false @Published var ctrlInitInProgress: Bool = false
@Published var notificationResponse: UNNotificationResponse?
// local authentication // local authentication
@Published var contentViewAccessAuthenticated: Bool = false @Published var contentViewAccessAuthenticated: Bool = false
@Published var laRequest: LocalAuthRequest? @Published var laRequest: LocalAuthRequest?
+29 -10
View File
@@ -29,17 +29,33 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
private var granted = false private var granted = false
private var prevNtfTime: Dictionary<ChatId, Date> = [:] private var prevNtfTime: Dictionary<ChatId, Date> = [:]
override init() {
super.init()
UNUserNotificationCenter.current().delegate = self
}
// Handle notification when app is in background // Handle notification when app is in background
func userNotificationCenter(_ center: UNUserNotificationCenter, func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse, didReceive response: UNNotificationResponse,
withCompletionHandler handler: () -> Void) { withCompletionHandler handler: () -> Void) {
logger.debug("NtfManager.userNotificationCenter: didReceive") logger.debug("NtfManager.userNotificationCenter: didReceive")
let content = response.notification.request.content if appStateGroupDefault.get() == .active {
processNotificationResponse(response)
} else {
logger.debug("NtfManager.userNotificationCenter: remember response in model")
ChatModel.shared.notificationResponse = response
}
handler()
}
func processNotificationResponse(_ ntfResponse: UNNotificationResponse) {
let chatModel = ChatModel.shared let chatModel = ChatModel.shared
let action = response.actionIdentifier let content = ntfResponse.notification.request.content
logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)") let action = ntfResponse.actionIdentifier
logger.debug("NtfManager.processNotificationResponse: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
if let userId = content.userInfo["userId"] as? Int64, if let userId = content.userInfo["userId"] as? Int64,
userId != chatModel.currentUser?.userId { userId != chatModel.currentUser?.userId {
logger.debug("NtfManager.processNotificationResponse changeActiveUser")
changeActiveUser(userId, viewPwd: nil) changeActiveUser(userId, viewPwd: nil)
} }
if content.categoryIdentifier == ntfCategoryContactRequest && (action == ntfActionAcceptContact || action == ntfActionAcceptContactIncognito), if content.categoryIdentifier == ntfCategoryContactRequest && (action == ntfActionAcceptContact || action == ntfActionAcceptContactIncognito),
@@ -61,7 +77,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
ItemsModel.shared.loadOpenChat(chatId) ItemsModel.shared.loadOpenChat(chatId)
} }
} }
handler()
} }
private func ntfCallAction(_ content: UNNotificationContent, _ action: String) -> (ChatId, NtfCallAction)? { private func ntfCallAction(_ content: UNNotificationContent, _ action: String) -> (ChatId, NtfCallAction)? {
@@ -76,7 +91,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
return nil return nil
} }
// Handle notification when the app is in foreground // Handle notification when the app is in foreground
func userNotificationCenter(_ center: UNUserNotificationCenter, func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification, willPresent notification: UNNotification,
@@ -185,6 +199,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
actions: [], actions: [],
intentIdentifiers: [], intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: NSLocalizedString("SimpleX encrypted message or connection event", comment: "notification") hiddenPreviewsBodyPlaceholder: NSLocalizedString("SimpleX encrypted message or connection event", comment: "notification")
),
UNNotificationCategory(
identifier: ntfCategoryManyEvents,
actions: [],
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: NSLocalizedString("New events", comment: "notification")
) )
]) ])
} }
@@ -210,29 +230,28 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
} }
} }
} }
center.delegate = self
} }
func notifyContactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest) { func notifyContactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest) {
logger.debug("NtfManager.notifyContactRequest") logger.debug("NtfManager.notifyContactRequest")
addNotification(createContactRequestNtf(user, contactRequest)) addNotification(createContactRequestNtf(user, contactRequest, 0))
} }
func notifyContactConnected(_ user: any UserLike, _ contact: Contact) { func notifyContactConnected(_ user: any UserLike, _ contact: Contact) {
logger.debug("NtfManager.notifyContactConnected") logger.debug("NtfManager.notifyContactConnected")
addNotification(createContactConnectedNtf(user, contact)) addNotification(createContactConnectedNtf(user, contact, 0))
} }
func notifyMessageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) { func notifyMessageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) {
logger.debug("NtfManager.notifyMessageReceived") logger.debug("NtfManager.notifyMessageReceived")
if cInfo.ntfsEnabled { if cInfo.ntfsEnabled {
addNotification(createMessageReceivedNtf(user, cInfo, cItem)) addNotification(createMessageReceivedNtf(user, cInfo, cItem, 0))
} }
} }
func notifyCallInvitation(_ invitation: RcvCallInvitation) { func notifyCallInvitation(_ invitation: RcvCallInvitation) {
logger.debug("NtfManager.notifyCallInvitation") logger.debug("NtfManager.notifyCallInvitation")
addNotification(createCallInvitationNtf(invitation)) addNotification(createCallInvitationNtf(invitation, 0))
} }
func setNtfBadgeCount(_ count: Int) { func setNtfBadgeCount(_ count: Int) {
+7 -1
View File
@@ -462,7 +462,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String
let r = chatSendCmdSync(.apiGetNtfToken) let r = chatSendCmdSync(.apiGetNtfToken)
switch r { switch r {
case let .ntfToken(token, status, ntfMode, ntfServer): return (token, status, ntfMode, ntfServer) case let .ntfToken(token, status, ntfMode, ntfServer): return (token, status, ntfMode, ntfServer)
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off, nil) case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED, _))): return (nil, nil, .off, nil)
default: default:
logger.debug("apiGetNtfToken response: \(String(describing: r))") logger.debug("apiGetNtfToken response: \(String(describing: r))")
return (nil, nil, .off, nil) return (nil, nil, .off, nil)
@@ -743,6 +743,12 @@ func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, Pendi
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
) )
return (nil, alert) return (nil, alert)
case .chatCmdError(_, .errorAgent(.SMP(_, .QUOTA))):
let alert = mkAlert(
title: "Undelivered messages",
message: "The connection reached the limit of undelivered messages, your contact may be offline."
)
return (nil, alert)
case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))): case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))):
if internalErr == "SEUniqueID" { if internalErr == "SEUniqueID" {
let alert = mkAlert( let alert = mkAlert(
+7 -1
View File
@@ -82,7 +82,12 @@ struct SimpleXApp: App {
if appState != .stopped { if appState != .stopped {
startChatAndActivate { startChatAndActivate {
if appState.inactive && chatModel.chatRunning == true { if chatModel.chatRunning == true {
if let ntfResponse = chatModel.notificationResponse {
chatModel.notificationResponse = nil
NtfManager.shared.processNotificationResponse(ntfResponse)
}
if appState.inactive {
Task { Task {
await updateChats() await updateChats()
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() { if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
@@ -92,6 +97,7 @@ struct SimpleXApp: App {
} }
} }
} }
}
default: default:
break break
} }
@@ -11,11 +11,11 @@ import SimpleXChat
struct CIChatFeatureView: View { struct CIChatFeatureView: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@Environment(\.revealed) var revealed: Bool
@ObservedObject var im = ItemsModel.shared @ObservedObject var im = ItemsModel.shared
@ObservedObject var chat: Chat @ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
var chatItem: ChatItem var chatItem: ChatItem
@Binding var revealed: Bool
var feature: Feature var feature: Feature
var icon: String? = nil var icon: String? = nil
var iconColor: Color var iconColor: Color
@@ -106,6 +106,9 @@ struct CIChatFeatureView: View {
struct CIChatFeatureView_Previews: PreviewProvider { struct CIChatFeatureView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let enabled = FeatureEnabled(forUser: false, forContact: false) let enabled = FeatureEnabled(forUser: false, forContact: false)
CIChatFeatureView(chat: Chat.sampleData, chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor(.secondary)) CIChatFeatureView(
chat: Chat.sampleData,
chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor(.secondary)
).environment(\.revealed, true)
} }
} }
@@ -285,17 +285,18 @@ struct CIFileView_Previews: PreviewProvider {
file: nil file: nil
) )
Group { Group {
ChatItemView(chat: Chat.sampleData, chatItem: sentFile, revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: sentFile)
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample())
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation))
ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile, revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile)
} }
.environment(\.revealed, false)
.previewLayout(.fixed(width: 360, height: 360)) .previewLayout(.fixed(width: 360, height: 360))
} }
} }
@@ -510,10 +510,10 @@ struct CIVoiceView_Previews: PreviewProvider {
duration: 30, duration: 30,
allowMenu: Binding.constant(true) allowMenu: Binding.constant(true)
) )
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false), allowMenu: .constant(true)) ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, allowMenu: .constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false), allowMenu: .constant(true)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), allowMenu: .constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false), allowMenu: .constant(true)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), allowMenu: .constant(true))
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, revealed: Binding.constant(false), allowMenu: .constant(true)) ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, allowMenu: .constant(true))
} }
.previewLayout(.fixed(width: 360, height: 360)) .previewLayout(.fixed(width: 360, height: 360))
} }
@@ -92,12 +92,13 @@ struct FramedCIVoiceView_Previews: PreviewProvider {
file: CIFile.getSample(fileStatus: .sndComplete) file: CIFile.getSample(fileStatus: .sndComplete)
) )
Group { Group {
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage)
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."))
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote, revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote)
} }
.environment(\.revealed, false)
.previewLayout(.fixed(width: 360, height: 360)) .previewLayout(.fixed(width: 360, height: 360))
} }
} }
File diff suppressed because one or more lines are too long
@@ -12,9 +12,9 @@ import SimpleXChat
struct MarkedDeletedItemView: View { struct MarkedDeletedItemView: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@Environment(\.revealed) var revealed: Bool
@ObservedObject var chat: Chat @ObservedObject var chat: Chat
var chatItem: ChatItem var chatItem: ChatItem
@Binding var revealed: Bool
var body: some View { var body: some View {
(Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText) (Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText)
@@ -79,7 +79,10 @@ struct MarkedDeletedItemView: View {
struct MarkedDeletedItemView_Previews: PreviewProvider { struct MarkedDeletedItemView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group { Group {
MarkedDeletedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true)) MarkedDeletedItemView(
chat: Chat.sampleData,
chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))
).environment(\.revealed, true)
} }
.previewLayout(.fixed(width: 360, height: 200)) .previewLayout(.fixed(width: 360, height: 200))
} }
+31 -28
View File
@@ -14,19 +14,28 @@ extension EnvironmentValues {
static let defaultValue: Bool = true static let defaultValue: Bool = true
} }
struct Revealed: EnvironmentKey {
static let defaultValue: Bool = true
}
var showTimestamp: Bool { var showTimestamp: Bool {
get { self[ShowTimestamp.self] } get { self[ShowTimestamp.self] }
set { self[ShowTimestamp.self] = newValue } set { self[ShowTimestamp.self] = newValue }
} }
var revealed: Bool {
get { self[Revealed.self] }
set { self[Revealed.self] = newValue }
}
} }
struct ChatItemView: View { struct ChatItemView: View {
@ObservedObject var chat: Chat @ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@Environment(\.showTimestamp) var showTimestamp: Bool @Environment(\.showTimestamp) var showTimestamp: Bool
@Environment(\.revealed) var revealed: Bool
var chatItem: ChatItem var chatItem: ChatItem
var maxWidth: CGFloat = .infinity var maxWidth: CGFloat = .infinity
@Binding var revealed: Bool
@Binding var allowMenu: Bool @Binding var allowMenu: Bool
init( init(
@@ -34,27 +43,25 @@ struct ChatItemView: View {
chatItem: ChatItem, chatItem: ChatItem,
showMember: Bool = false, showMember: Bool = false,
maxWidth: CGFloat = .infinity, maxWidth: CGFloat = .infinity,
revealed: Binding<Bool>,
allowMenu: Binding<Bool> = .constant(false) allowMenu: Binding<Bool> = .constant(false)
) { ) {
self.chat = chat self.chat = chat
self.chatItem = chatItem self.chatItem = chatItem
self.maxWidth = maxWidth self.maxWidth = maxWidth
_revealed = revealed
_allowMenu = allowMenu _allowMenu = allowMenu
} }
var body: some View { var body: some View {
let ci = chatItem let ci = chatItem
if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) { if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) {
MarkedDeletedItemView(chat: chat, chatItem: chatItem, revealed: $revealed) MarkedDeletedItemView(chat: chat, chatItem: chatItem)
} else if ci.quotedItem == nil && ci.meta.itemForwarded == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive { } else if ci.quotedItem == nil && ci.meta.itemForwarded == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) { if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
EmojiItemView(chat: chat, chatItem: ci) EmojiItemView(chat: chat, chatItem: ci)
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent { } else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: $allowMenu) CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: $allowMenu)
} else if ci.content.msgContent == nil { } else if ci.content.msgContent == nil {
ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case ChatItemContentView(chat: chat, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
} else { } else {
framedItemView() framedItemView()
} }
@@ -84,7 +91,6 @@ struct ChatItemView: View {
chat: chat, chat: chat,
chatItem: chatItem, chatItem: chatItem,
preview: preview, preview: preview,
revealed: $revealed,
maxWidth: maxWidth, maxWidth: maxWidth,
imgWidth: adjustedMaxWidth, imgWidth: adjustedMaxWidth,
videoWidth: adjustedMaxWidth, videoWidth: adjustedMaxWidth,
@@ -96,9 +102,9 @@ struct ChatItemView: View {
struct ChatItemContentView<Content: View>: View { struct ChatItemContentView<Content: View>: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@Environment(\.revealed) var revealed: Bool
@ObservedObject var chat: Chat @ObservedObject var chat: Chat
var chatItem: ChatItem var chatItem: ChatItem
@Binding var revealed: Bool
var msgContentView: () -> Content var msgContentView: () -> Content
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@@ -130,7 +136,7 @@ struct ChatItemContentView<Content: View>: View {
case let .rcvChatPreference(feature, allowed, param): case let .rcvChatPreference(feature, allowed, param):
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param) CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
case let .sndChatPreference(feature, _, _): case let .sndChatPreference(feature, _, _):
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary) CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary)
case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary)) case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary)) case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red) case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
@@ -177,7 +183,7 @@ struct ChatItemContentView<Content: View>: View {
} }
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View { private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor) CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, iconColor: iconColor)
} }
private var mergedGroupEventText: Text? { private var mergedGroupEventText: Text? {
@@ -238,16 +244,17 @@ func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text {
struct ChatItemView_Previews: PreviewProvider { struct ChatItemView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group{ Group{
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample())
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)))
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true)).environment(\.revealed, true)
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true)) ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true)).environment(\.revealed, true)
} }
.environment(\.revealed, false)
.previewLayout(.fixed(width: 360, height: 70)) .previewLayout(.fixed(width: 360, height: 70))
.environmentObject(Chat.sampleData) .environmentObject(Chat.sampleData)
} }
@@ -265,8 +272,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
content: .rcvIntegrityError(msgError: .msgSkipped(fromMsgId: 1, toMsgId: 2)), content: .rcvIntegrityError(msgError: .msgSkipped(fromMsgId: 1, toMsgId: 2)),
quotedItem: nil, quotedItem: nil,
file: nil file: nil
), )
revealed: Binding.constant(true)
) )
ChatItemView( ChatItemView(
chat: Chat.sampleData, chat: Chat.sampleData,
@@ -276,8 +282,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
content: .rcvDecryptionError(msgDecryptError: .ratchetHeader, msgCount: 2), content: .rcvDecryptionError(msgDecryptError: .ratchetHeader, msgCount: 2),
quotedItem: nil, quotedItem: nil,
file: nil file: nil
), )
revealed: Binding.constant(true)
) )
ChatItemView( ChatItemView(
chat: Chat.sampleData, chat: Chat.sampleData,
@@ -287,8 +292,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
content: .rcvGroupInvitation(groupInvitation: CIGroupInvitation.getSample(status: .pending), memberRole: .admin), content: .rcvGroupInvitation(groupInvitation: CIGroupInvitation.getSample(status: .pending), memberRole: .admin),
quotedItem: nil, quotedItem: nil,
file: nil file: nil
), )
revealed: Binding.constant(true)
) )
ChatItemView( ChatItemView(
chat: Chat.sampleData, chat: Chat.sampleData,
@@ -298,8 +302,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
content: .rcvGroupEvent(rcvGroupEvent: .memberAdded(groupMemberId: 1, profile: Profile.sampleData)), content: .rcvGroupEvent(rcvGroupEvent: .memberAdded(groupMemberId: 1, profile: Profile.sampleData)),
quotedItem: nil, quotedItem: nil,
file: nil file: nil
), )
revealed: Binding.constant(true)
) )
ChatItemView( ChatItemView(
chat: Chat.sampleData, chat: Chat.sampleData,
@@ -309,10 +312,10 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
content: ciFeatureContent, content: ciFeatureContent,
quotedItem: nil, quotedItem: nil,
file: nil file: nil
), )
revealed: Binding.constant(true)
) )
} }
.environment(\.revealed, true)
.previewLayout(.fixed(width: 360, height: 70)) .previewLayout(.fixed(width: 360, height: 70))
.environmentObject(Chat.sampleData) .environmentObject(Chat.sampleData)
} }
+2 -2
View File
@@ -1183,11 +1183,11 @@ struct ChatView: View {
chat: chat, chat: chat,
chatItem: ci, chatItem: ci,
maxWidth: maxWidth, maxWidth: maxWidth,
revealed: .constant(revealed),
allowMenu: $allowMenu allowMenu: $allowMenu
) )
.environment(\.revealed, revealed)
.environment(\.showTimestamp, itemSeparation.timestamp) .environment(\.showTimestamp, itemSeparation.timestamp)
.modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap)) .modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap && (ci.meta.itemDeleted == nil || revealed)))
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) } .contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
.accessibilityLabel("") .accessibilityLabel("")
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 { if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
@@ -287,8 +287,8 @@ struct ComposeView: View {
// this is a workaround to fire an explicit event in certain cases // this is a workaround to fire an explicit event in certain cases
@State private var stopPlayback: Bool = false @State private var stopPlayback: Bool = false
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @UserDefault(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -40,7 +40,7 @@ struct SendMessageView: View {
@State private var showCustomTimePicker = false @State private var showCustomTimePicker = false
@State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get() @State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get()
@State private var progressByTimeout = false @State private var progressByTimeout = false
@AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false @UserDefault(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
var body: some View { var body: some View {
ZStack { ZStack {
+1 -1
View File
@@ -78,7 +78,7 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
self.dataSource = UITableViewDiffableDataSource<Section, ChatItem>( self.dataSource = UITableViewDiffableDataSource<Section, ChatItem>(
tableView: tableView tableView: tableView
) { (tableView, indexPath, item) -> UITableViewCell? in ) { (tableView, indexPath, item) -> UITableViewCell? in
if indexPath.item > self.itemCount - 8, self.itemCount > 8 { if indexPath.item > self.itemCount - 8 {
self.representer.loadPage() self.representer.loadPage()
} }
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath) let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
@@ -121,9 +121,11 @@ struct ChatListView: View {
UserPicker(userPickerShown: $userPickerShown, activeSheet: $activeUserPickerSheet) UserPicker(userPickerShown: $userPickerShown, activeSheet: $activeUserPickerSheet)
} }
) )
.sheet(item: $activeUserPickerSheet) { .appSheet(
UserPickerSheetView(sheet: $0) item: $activeUserPickerSheet,
} onDismiss: { chatModel.laRequest = nil },
content: { UserPickerSheetView(sheet: $0) }
)
.onChange(of: activeUserPickerSheet) { .onChange(of: activeUserPickerSheet) {
if $0 != nil { if $0 != nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+7 -5
View File
@@ -11,6 +11,12 @@ import SwiftUI
class AppSheetState: ObservableObject { class AppSheetState: ObservableObject {
static let shared = AppSheetState() static let shared = AppSheetState()
@Published var scenePhaseActive: Bool = false @Published var scenePhaseActive: Bool = false
func redactionReasons(_ protectScreen: Bool) -> RedactionReasons {
!protectScreen || scenePhaseActive
? RedactionReasons()
: RedactionReasons.placeholder
}
} }
private struct PrivacySensitive: ViewModifier { private struct PrivacySensitive: ViewModifier {
@@ -19,11 +25,7 @@ private struct PrivacySensitive: ViewModifier {
@ObservedObject var appSheetState: AppSheetState = AppSheetState.shared @ObservedObject var appSheetState: AppSheetState = AppSheetState.shared
func body(content: Content) -> some View { func body(content: Content) -> some View {
if !protectScreen { content.redacted(reason: appSheetState.redactionReasons(protectScreen))
content
} else {
content.privacySensitive(!appSheetState.scenePhaseActive).redacted(reason: .privacy)
}
} }
} }
@@ -36,12 +36,7 @@ struct ChatItemClipped: ViewModifier {
.sndMsgContent, .sndMsgContent,
.rcvMsgContent, .rcvMsgContent,
.rcvDecryptionError, .rcvDecryptionError,
.sndDeleted,
.rcvDeleted,
.rcvIntegrityError, .rcvIntegrityError,
.sndModerated,
.rcvModerated,
.rcvBlocked,
.invalidJSON: .invalidJSON:
let tail = if let mc = ci.content.msgContent, mc.isImageOrVideo && mc.text.isEmpty { let tail = if let mc = ci.content.msgContent, mc.isImageOrVideo && mc.text.isEmpty {
false false
@@ -16,6 +16,7 @@ struct UserWallpaperEditor: View {
@State var themeModeOverride: ThemeModeOverride @State var themeModeOverride: ThemeModeOverride
@State var applyToMode: DefaultThemeMode? @State var applyToMode: DefaultThemeMode?
@State var showMore: Bool = false @State var showMore: Bool = false
@State var showFileImporter: Bool = false
@Binding var globalThemeUsed: Bool @Binding var globalThemeUsed: Bool
var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void
@@ -125,7 +126,13 @@ struct UserWallpaperEditor: View {
CustomizeThemeColorsSection(editColor: { name in editColor(name, theme) }) CustomizeThemeColorsSection(editColor: { name in editColor(name, theme) })
ImportExportThemeSection(perChat: nil, perUser: ChatModel.shared.currentUser?.uiThemes) { imported in ImportExportThemeSection(showFileImporter: $showFileImporter, perChat: nil, perUser: ChatModel.shared.currentUser?.uiThemes)
} else {
AdvancedSettingsButton(theme.colors.primary) { showMore = true }
}
}
.modifier(
ThemeImporter(isPresented: $showFileImporter) { imported in
let importedFromString = imported.wallpaper?.importFromString() let importedFromString = imported.wallpaper?.importFromString()
let importedType = importedFromString?.toAppWallpaper().type let importedType = importedFromString?.toAppWallpaper().type
let currentTheme = ThemeManager.currentColors(nil, nil, nil, themeOverridesDefault.get()) let currentTheme = ThemeManager.currentColors(nil, nil, nil, themeOverridesDefault.get())
@@ -139,10 +146,7 @@ struct UserWallpaperEditor: View {
await save(applyToMode, res) await save(applyToMode, res)
} }
} }
} else { )
AdvancedSettingsButton(theme.colors.primary) { showMore = true }
}
}
} }
private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool { private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool {
@@ -216,6 +220,7 @@ struct ChatWallpaperEditor: View {
@State var themeModeOverride: ThemeModeOverride @State var themeModeOverride: ThemeModeOverride
@State var applyToMode: DefaultThemeMode? @State var applyToMode: DefaultThemeMode?
@State var showMore: Bool = false @State var showMore: Bool = false
@State var showFileImporter: Bool = false
@Binding var globalThemeUsed: Bool @Binding var globalThemeUsed: Bool
var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void
@@ -328,7 +333,13 @@ struct ChatWallpaperEditor: View {
CustomizeThemeColorsSection(editColor: editColor) CustomizeThemeColorsSection(editColor: editColor)
ImportExportThemeSection(perChat: themeModeOverride, perUser: ChatModel.shared.currentUser?.uiThemes) { imported in ImportExportThemeSection(showFileImporter: $showFileImporter, perChat: themeModeOverride, perUser: ChatModel.shared.currentUser?.uiThemes)
} else {
AdvancedSettingsButton(theme.colors.primary) { showMore = true }
}
}
.modifier(
ThemeImporter(isPresented: $showFileImporter) { imported in
let importedFromString = imported.wallpaper?.importFromString() let importedFromString = imported.wallpaper?.importFromString()
let importedType = importedFromString?.toAppWallpaper().type let importedType = importedFromString?.toAppWallpaper().type
let currentTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) let currentTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
@@ -342,10 +353,7 @@ struct ChatWallpaperEditor: View {
await save(applyToMode, res) await save(applyToMode, res)
} }
} }
} else { )
AdvancedSettingsButton(theme.colors.primary) { showMore = true }
}
}
} }
private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool { private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool {
@@ -0,0 +1,62 @@
//
// UserDefault.swift
// SimpleX (iOS)
//
// Created by user on 14/10/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import Combine
@propertyWrapper
public struct UserDefault<Value: Equatable>: DynamicProperty {
@StateObject private var observer = UserDefaultObserver()
let initialValue: Value
let key: String
let store: UserDefaults
public init(
wrappedValue: Value,
_ key: String,
store: UserDefaults = .standard
) {
self.initialValue = wrappedValue
self.key = key
self.store = store
}
public var wrappedValue: Value {
get {
// Observer can only be accessed after the property wrapper is installed in view (runtime exception)
observer.subscribe(to: key)
return store.object(forKey: key) as? Value ?? initialValue
}
nonmutating set {
store.set(newValue, forKey: key)
}
}
}
private class UserDefaultObserver: ObservableObject {
private var subscribed = false
func subscribe(to key: String) {
if !subscribed {
NotificationCenter.default.addObserver(
self,
selector: #selector(userDefaultsDidChange),
name: UserDefaults.didChangeNotification,
object: nil
)
subscribed = true
}
}
@objc
private func userDefaultsDidChange(_ notification: Notification) {
Task { @MainActor in objectWillChange.send() }
}
deinit { NotificationCenter.default.removeObserver(self) }
}
+6 -1
View File
@@ -105,7 +105,12 @@ struct TerminalView: View {
} }
} }
.navigationViewStyle(.stack) .navigationViewStyle(.stack)
.navigationTitle("Chat console") .toolbar {
// Redaction broken for `.navigationTitle` - using a toolbar item instead.
ToolbarItem(placement: .principal) {
Text("Chat console").font(.headline)
}
}
.modifier(ThemedBackground()) .modifier(ThemedBackground())
} }
@@ -367,13 +367,13 @@ struct ChatThemePreview: View {
let alice = ChatItem.getSample(1, CIDirection.directRcv, Date.now, NSLocalizedString("Good afternoon!", comment: "message preview")) let alice = ChatItem.getSample(1, CIDirection.directRcv, Date.now, NSLocalizedString("Good afternoon!", comment: "message preview"))
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir)) let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
HStack { HStack {
ChatItemView(chat: Chat.sampleData, chatItem: alice, revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: alice)
.modifier(ChatItemClipped(alice, tailVisible: true)) .modifier(ChatItemClipped(alice, tailVisible: true))
Spacer() Spacer()
} }
HStack { HStack {
Spacer() Spacer()
ChatItemView(chat: Chat.sampleData, chatItem: bob, revealed: Binding.constant(false)) ChatItemView(chat: Chat.sampleData, chatItem: bob)
.modifier(ChatItemClipped(bob, tailVisible: true)) .modifier(ChatItemClipped(bob, tailVisible: true))
.frame(alignment: .trailing) .frame(alignment: .trailing)
} }
@@ -583,11 +583,14 @@ struct CustomizeThemeView: View {
} }
} }
ImportExportThemeSection(perChat: nil, perUser: nil, save: { theme in ImportExportThemeSection(showFileImporter: $showFileImporter, perChat: nil, perUser: nil)
}
.modifier(
ThemeImporter(isPresented: $showFileImporter) { theme in
ThemeManager.saveAndApplyThemeOverrides(theme) ThemeManager.saveAndApplyThemeOverrides(theme)
saveThemeToDatabase(nil) saveThemeToDatabase(nil)
})
} }
)
/// When changing app theme, user overrides are hidden. User overrides will be returned back after closing Appearance screen, see ThemeDestinationPicker() /// When changing app theme, user overrides are hidden. User overrides will be returned back after closing Appearance screen, see ThemeDestinationPicker()
.interactiveDismissDisabled(true) .interactiveDismissDisabled(true)
} }
@@ -595,10 +598,9 @@ struct CustomizeThemeView: View {
struct ImportExportThemeSection: View { struct ImportExportThemeSection: View {
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@Binding var showFileImporter: Bool
var perChat: ThemeModeOverride? var perChat: ThemeModeOverride?
var perUser: ThemeModeOverrides? var perUser: ThemeModeOverrides?
var save: (ThemeOverrides) -> Void
@State private var showFileImporter = false
var body: some View { var body: some View {
Section { Section {
@@ -626,8 +628,17 @@ struct ImportExportThemeSection: View {
} label: { } label: {
Text("Import theme").foregroundColor(theme.colors.primary) Text("Import theme").foregroundColor(theme.colors.primary)
} }
.fileImporter( }
isPresented: $showFileImporter, }
}
struct ThemeImporter: ViewModifier {
@Binding var isPresented: Bool
var save: (ThemeOverrides) -> Void
func body(content: Content) -> some View {
content.fileImporter(
isPresented: $isPresented,
allowedContentTypes: [.data/*.plainText*/], allowedContentTypes: [.data/*.plainText*/],
allowsMultipleSelection: false allowsMultipleSelection: false
) { result in ) { result in
@@ -662,7 +673,6 @@ struct ImportExportThemeSection: View {
} }
} }
} }
}
} }
struct UserWallpaperEditorSheet: View { struct UserWallpaperEditorSheet: View {
@@ -331,7 +331,12 @@ struct SettingsView: View {
chatDatabaseRow() chatDatabaseRow()
NavigationLink { NavigationLink {
MigrateFromDevice(showProgressOnSettings: $showProgress) MigrateFromDevice(showProgressOnSettings: $showProgress)
.navigationTitle("Migrate device") .toolbar {
// Redaction broken for `.navigationTitle` - using a toolbar item instead.
ToolbarItem(placement: .principal) {
Text("Migrate device").font(.headline)
}
}
.modifier(ThemedBackground(grouped: true)) .modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.large)
} label: { } label: {
@@ -1087,6 +1087,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better calls" xml:space="preserve"> <trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source> <source>Better calls</source>
<target>Verbesserte Anrufe</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better groups" xml:space="preserve"> <trans-unit id="Better groups" xml:space="preserve">
@@ -1096,6 +1097,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better message dates." xml:space="preserve"> <trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source> <source>Better message dates.</source>
<target>Verbesserte Nachrichten-Datumsinformation</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better messages" xml:space="preserve"> <trans-unit id="Better messages" xml:space="preserve">
@@ -1110,14 +1112,17 @@
</trans-unit> </trans-unit>
<trans-unit id="Better notifications" xml:space="preserve"> <trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source> <source>Better notifications</source>
<target>Verbesserte Benachrichtigungen</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve"> <trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source> <source>Better security ✅</source>
<target>Verbesserte Sicherheit ✅</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better user experience" xml:space="preserve"> <trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source> <source>Better user experience</source>
<target>Verbesserte Nutzer-Erfahrung</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Black" xml:space="preserve"> <trans-unit id="Black" xml:space="preserve">
@@ -1942,6 +1947,7 @@ Das ist Ihr eigener Einmal-Link!</target>
</trans-unit> </trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve"> <trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source> <source>Customizable message shape.</source>
<target>Anpassbares Format des Nachrichtenfelds</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Customize theme" xml:space="preserve"> <trans-unit id="Customize theme" xml:space="preserve">
@@ -2240,6 +2246,7 @@ Das ist Ihr eigener Einmal-Link!</target>
</trans-unit> </trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source> <source>Delete or moderate up to 200 messages.</source>
<target>Bis zu 200 Nachrichten löschen oder moderieren</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve"> <trans-unit id="Delete pending connection?" xml:space="preserve">
@@ -3367,6 +3374,7 @@ Das ist Ihr eigener Einmal-Link!</target>
</trans-unit> </trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> <trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source> <source>Forward up to 20 messages at once.</source>
<target>Bis zu 20 Nachrichten auf einmal weiterleiten</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Forwarded" xml:space="preserve"> <trans-unit id="Forwarded" xml:space="preserve">
@@ -3761,6 +3769,8 @@ Fehler: %2$@</target>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve"> <trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage. <source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source> More improvements are coming soon!</source>
<target>Verbesserte Nachrichten-Auslieferung und verringerter Datenverbrauch.
Weitere Verbesserungen sind bald verfügbar!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve"> <trans-unit id="Improved message delivery" xml:space="preserve">
@@ -6463,6 +6473,7 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
</trans-unit> </trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source> <source>SimpleX protocols reviewed by Trail of Bits.</source>
<target>Die SimpleX-Protokolle wurden von Trail of Bits überprüft.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve"> <trans-unit id="Simplified incognito mode" xml:space="preserve">
@@ -6642,10 +6653,12 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
</trans-unit> </trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve"> <trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source> <source>Switch audio and video during the call.</source>
<target>Während des Anrufs zwischen Audio und Video wechseln</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source> <source>Switch chat profile for 1-time invitations.</source>
<target>Das Chat-Profil für Einmal-Einladungen wechseln</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="System" xml:space="preserve"> <trans-unit id="System" xml:space="preserve">
@@ -1087,6 +1087,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better calls" xml:space="preserve"> <trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source> <source>Better calls</source>
<target>Llamadas mejoradas</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better groups" xml:space="preserve"> <trans-unit id="Better groups" xml:space="preserve">
@@ -1096,6 +1097,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better message dates." xml:space="preserve"> <trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source> <source>Better message dates.</source>
<target>Sistema de fechas mejorado.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better messages" xml:space="preserve"> <trans-unit id="Better messages" xml:space="preserve">
@@ -1110,14 +1112,17 @@
</trans-unit> </trans-unit>
<trans-unit id="Better notifications" xml:space="preserve"> <trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source> <source>Better notifications</source>
<target>Notificaciones mejoradas</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve"> <trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source> <source>Better security ✅</source>
<target>Seguridad mejorada ✅</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better user experience" xml:space="preserve"> <trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source> <source>Better user experience</source>
<target>Experiencia de usuario mejorada</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Black" xml:space="preserve"> <trans-unit id="Black" xml:space="preserve">
@@ -1942,6 +1947,7 @@ This is your own one-time link!</source>
</trans-unit> </trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve"> <trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source> <source>Customizable message shape.</source>
<target>Forma personalizable de los mensajes.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Customize theme" xml:space="preserve"> <trans-unit id="Customize theme" xml:space="preserve">
@@ -2240,6 +2246,7 @@ This is your own one-time link!</source>
</trans-unit> </trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source> <source>Delete or moderate up to 200 messages.</source>
<target>Borra o modera hasta 200 mensajes a la vez.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve"> <trans-unit id="Delete pending connection?" xml:space="preserve">
@@ -3367,6 +3374,7 @@ This is your own one-time link!</source>
</trans-unit> </trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> <trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source> <source>Forward up to 20 messages at once.</source>
<target>Desplazamiento de hasta 20 mensajes.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Forwarded" xml:space="preserve"> <trans-unit id="Forwarded" xml:space="preserve">
@@ -3761,6 +3769,8 @@ Error: %2$@</target>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve"> <trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage. <source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source> More improvements are coming soon!</source>
<target>Reducción del tráfico y entrega mejorada.
¡Pronto habrá nuevas mejoras!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve"> <trans-unit id="Improved message delivery" xml:space="preserve">
@@ -6463,6 +6473,7 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
</trans-unit> </trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source> <source>SimpleX protocols reviewed by Trail of Bits.</source>
<target>Protocolos de SimpleX auditados por Trail of Bits.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve"> <trans-unit id="Simplified incognito mode" xml:space="preserve">
@@ -6642,10 +6653,12 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
</trans-unit> </trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve"> <trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source> <source>Switch audio and video during the call.</source>
<target>Intercambia audio y video durante la llamada.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source> <source>Switch chat profile for 1-time invitations.</source>
<target>Cambia el perfil de chat para invitaciones de un solo uso.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="System" xml:space="preserve"> <trans-unit id="System" xml:space="preserve">
@@ -762,7 +762,7 @@
</trans-unit> </trans-unit>
<trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve"> <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve">
<source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source>
<target>Az ismerőseivel kapcsolatban marad. A profil változtatások frissítésre kerülnek az ismerősöknél.</target> <target>Az ismerőseivel kapcsolatban marad. A profil-változtatások frissítésre kerülnek az ismerősöknél.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve"> <trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve">
@@ -1087,6 +1087,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better calls" xml:space="preserve"> <trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source> <source>Better calls</source>
<target>Továbbfejlesztett hívásélmény</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better groups" xml:space="preserve"> <trans-unit id="Better groups" xml:space="preserve">
@@ -1096,6 +1097,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better message dates." xml:space="preserve"> <trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source> <source>Better message dates.</source>
<target>Továbbfejlesztett üzenetdátumok.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better messages" xml:space="preserve"> <trans-unit id="Better messages" xml:space="preserve">
@@ -1110,14 +1112,17 @@
</trans-unit> </trans-unit>
<trans-unit id="Better notifications" xml:space="preserve"> <trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source> <source>Better notifications</source>
<target>Továbbfejlesztett értesítések</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve"> <trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source> <source>Better security ✅</source>
<target>Továbbfejlesztett biztonság ✅</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better user experience" xml:space="preserve"> <trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source> <source>Better user experience</source>
<target>Továbbfejlesztett felhasználói élmény</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Black" xml:space="preserve"> <trans-unit id="Black" xml:space="preserve">
@@ -1408,7 +1413,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Chat profile" xml:space="preserve"> <trans-unit id="Chat profile" xml:space="preserve">
<source>Chat profile</source> <source>Chat profile</source>
<target>Felhasználói profil</target> <target>Csevegési profil</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Chat theme" xml:space="preserve"> <trans-unit id="Chat theme" xml:space="preserve">
@@ -1433,7 +1438,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve"> <trans-unit id="Choose _Migrate from another device_ on the new device and scan QR code." xml:space="preserve">
<source>Choose _Migrate from another device_ on the new device and scan QR code.</source> <source>Choose _Migrate from another device_ on the new device and scan QR code.</source>
<target>Válassza az _Átköltöztetés egy másik eszközről opciót az új eszközén és olvassa be a QR-kódot.</target> <target>Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközén és olvassa be a QR-kódot.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Choose file" xml:space="preserve"> <trans-unit id="Choose file" xml:space="preserve">
@@ -1717,7 +1722,7 @@ Ez az Ön egyszer használható hivatkozása!</target>
</trans-unit> </trans-unit>
<trans-unit id="Connection timeout" xml:space="preserve"> <trans-unit id="Connection timeout" xml:space="preserve">
<source>Connection timeout</source> <source>Connection timeout</source>
<target>Kapcsolat időtúllépés</target> <target>Időtúllépés kapcsolódáskor</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Connection with desktop stopped" xml:space="preserve"> <trans-unit id="Connection with desktop stopped" xml:space="preserve">
@@ -1942,6 +1947,7 @@ Ez az Ön egyszer használható hivatkozása!</target>
</trans-unit> </trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve"> <trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source> <source>Customizable message shape.</source>
<target>Testreszabható üzenetbuborékok.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Customize theme" xml:space="preserve"> <trans-unit id="Customize theme" xml:space="preserve">
@@ -1971,7 +1977,7 @@ Ez az Ön egyszer használható hivatkozása!</target>
</trans-unit> </trans-unit>
<trans-unit id="Database IDs and Transport isolation option." xml:space="preserve"> <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve">
<source>Database IDs and Transport isolation option.</source> <source>Database IDs and Transport isolation option.</source>
<target>Adatbázis-azonosítók és átviteli izolációs beállítások.</target> <target>Adatbázis-azonosítók és átvitel-izolációs beállítások.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Database downgrade" xml:space="preserve"> <trans-unit id="Database downgrade" xml:space="preserve">
@@ -2240,6 +2246,7 @@ Ez az Ön egyszer használható hivatkozása!</target>
</trans-unit> </trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source> <source>Delete or moderate up to 200 messages.</source>
<target>Legfeljebb 200 üzenet egyszerre való törlése, vagy moderálása.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve"> <trans-unit id="Delete pending connection?" xml:space="preserve">
@@ -2349,7 +2356,7 @@ Ez az Ön egyszer használható hivatkozása!</target>
</trans-unit> </trans-unit>
<trans-unit id="Details" xml:space="preserve"> <trans-unit id="Details" xml:space="preserve">
<source>Details</source> <source>Details</source>
<target>Részletek</target> <target>További részletek</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Develop" xml:space="preserve"> <trans-unit id="Develop" xml:space="preserve">
@@ -2384,7 +2391,7 @@ Ez az Ön egyszer használható hivatkozása!</target>
</trans-unit> </trans-unit>
<trans-unit id="Different names, avatars and transport isolation." xml:space="preserve"> <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve">
<source>Different names, avatars and transport isolation.</source> <source>Different names, avatars and transport isolation.</source>
<target>Különböző nevek, avatarok és átviteli izoláció.</target> <target>Különböző nevek, profilképek és átvitel-izoláció.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Direct messages" xml:space="preserve"> <trans-unit id="Direct messages" xml:space="preserve">
@@ -2910,7 +2917,7 @@ Ez az Ön egyszer használható hivatkozása!</target>
</trans-unit> </trans-unit>
<trans-unit id="Error deleting user profile" xml:space="preserve"> <trans-unit id="Error deleting user profile" xml:space="preserve">
<source>Error deleting user profile</source> <source>Error deleting user profile</source>
<target>Hiba a felhasználói profil törlésekor</target> <target>Hiba a felhasználó-profil törlésekor</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Error downloading the archive" xml:space="preserve"> <trans-unit id="Error downloading the archive" xml:space="preserve">
@@ -3025,7 +3032,7 @@ Ez az Ön egyszer használható hivatkozása!</target>
</trans-unit> </trans-unit>
<trans-unit id="Error saving user password" xml:space="preserve"> <trans-unit id="Error saving user password" xml:space="preserve">
<source>Error saving user password</source> <source>Error saving user password</source>
<target>Hiba a felhasználó jelszavának mentésekor</target> <target>Hiba a felhasználói jelszó mentésekor</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Error scanning code: %@" xml:space="preserve"> <trans-unit id="Error scanning code: %@" xml:space="preserve">
@@ -3095,7 +3102,7 @@ Ez az Ön egyszer használható hivatkozása!</target>
</trans-unit> </trans-unit>
<trans-unit id="Error updating user privacy" xml:space="preserve"> <trans-unit id="Error updating user privacy" xml:space="preserve">
<source>Error updating user privacy</source> <source>Error updating user privacy</source>
<target>Hiba a felhasználói beállítások frissítésekor</target> <target>Hiba a felhasználói adatvédelem frissítésekor</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Error uploading the archive" xml:space="preserve"> <trans-unit id="Error uploading the archive" xml:space="preserve">
@@ -3367,6 +3374,7 @@ Ez az Ön egyszer használható hivatkozása!</target>
</trans-unit> </trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> <trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source> <source>Forward up to 20 messages at once.</source>
<target>Legfeljebb 20 üzenet egyszerre való továbbítása.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Forwarded" xml:space="preserve"> <trans-unit id="Forwarded" xml:space="preserve">
@@ -3615,7 +3623,7 @@ Hiba: %2$@</target>
</trans-unit> </trans-unit>
<trans-unit id="Hide" xml:space="preserve"> <trans-unit id="Hide" xml:space="preserve">
<source>Hide</source> <source>Hide</source>
<target>Elrejtés</target> <target>Összecsukás</target>
<note>chat item action</note> <note>chat item action</note>
</trans-unit> </trans-unit>
<trans-unit id="Hide app screen in the recent apps." xml:space="preserve"> <trans-unit id="Hide app screen in the recent apps." xml:space="preserve">
@@ -3700,7 +3708,7 @@ Hiba: %2$@</target>
</trans-unit> </trans-unit>
<trans-unit id="If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." xml:space="preserve"> <trans-unit id="If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." xml:space="preserve">
<source>If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).</source> <source>If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).</source>
<target>Ha most kell használnia a csevegést, koppintson alább az **Befejezés később** lehetőségre (az alkalmazás újraindításakor felajánlásra kerül az adatbázis átköltöztetése).</target> <target>Ha most kell használnia a csevegést, koppintson alább a **Befejezés később** lehetőségre (az alkalmazás újraindításakor felajánlásra kerül az adatbázis átköltöztetése).</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Ignore" xml:space="preserve"> <trans-unit id="Ignore" xml:space="preserve">
@@ -3761,6 +3769,8 @@ Hiba: %2$@</target>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve"> <trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage. <source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source> More improvements are coming soon!</source>
<target>Továbbfejlesztett kézbesítés, csökkentett adatforgalom-használat.
További fejlesztések hamarosan!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve"> <trans-unit id="Improved message delivery" xml:space="preserve">
@@ -4205,7 +4215,7 @@ Ez az Ön hivatkozása a(z) %@ csoporthoz!</target>
</trans-unit> </trans-unit>
<trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve">
<source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source>
<target>Sokan kérdezték: *ha a SimpleX Chatnek nincsenek felhasználói azonosítói, akkor hogyan tud üzeneteket kézbesíteni?*</target> <target>Sokan kérdezték: *ha a SimpleX Chatnek nincsenek felhasználó-azonosítói, akkor hogyan tud üzeneteket kézbesíteni?*</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Mark deleted for everyone" xml:space="preserve"> <trans-unit id="Mark deleted for everyone" xml:space="preserve">
@@ -4783,7 +4793,7 @@ VPN engedélyezése szükséges.</target>
</trans-unit> </trans-unit>
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
<target>Csak az eszközök alkalmazásai tárolják a felhasználói profilokat, névjegyeket, csoportokat és a **2 rétegű végpontok közötti titkosítással** küldött üzeneteket.</target> <target>Csak az eszközök alkalmazásai tárolják a felhasználó-profilokat, névjegyeket, csoportokat és a **2 rétegű végpontok közötti titkosítással** küldött üzeneteket.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Only delete conversation" xml:space="preserve"> <trans-unit id="Only delete conversation" xml:space="preserve">
@@ -4893,7 +4903,7 @@ VPN engedélyezése szükséges.</target>
</trans-unit> </trans-unit>
<trans-unit id="Open user profiles" xml:space="preserve"> <trans-unit id="Open user profiles" xml:space="preserve">
<source>Open user profiles</source> <source>Open user profiles</source>
<target>Felhasználói profilok megnyitása</target> <target>Felhasználó-profilok megnyitása</target>
<note>authentication reason</note> <note>authentication reason</note>
</trans-unit> </trans-unit>
<trans-unit id="Open-source protocol and code anybody can run the servers." xml:space="preserve"> <trans-unit id="Open-source protocol and code anybody can run the servers." xml:space="preserve">
@@ -4945,12 +4955,12 @@ VPN engedélyezése szükséges.</target>
</trans-unit> </trans-unit>
<trans-unit id="PING count" xml:space="preserve"> <trans-unit id="PING count" xml:space="preserve">
<source>PING count</source> <source>PING count</source>
<target>PING számláló</target> <target>PING-ek száma</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="PING interval" xml:space="preserve"> <trans-unit id="PING interval" xml:space="preserve">
<source>PING interval</source> <source>PING interval</source>
<target>PING időköze</target> <target>Időtartam a PING-ek között</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Passcode" xml:space="preserve"> <trans-unit id="Passcode" xml:space="preserve">
@@ -5306,12 +5316,12 @@ Engedélyezze a „Beállítások -&gt; Hálózat és kiszolgálók” menüben.
</trans-unit> </trans-unit>
<trans-unit id="Protocol timeout" xml:space="preserve"> <trans-unit id="Protocol timeout" xml:space="preserve">
<source>Protocol timeout</source> <source>Protocol timeout</source>
<target>Protokoll időtúllépés</target> <target>Protokoll időtúllépése</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Protocol timeout per KB" xml:space="preserve"> <trans-unit id="Protocol timeout per KB" xml:space="preserve">
<source>Protocol timeout per KB</source> <source>Protocol timeout per KB</source>
<target>Protokoll időkorlát KB-onként</target> <target>Protokoll időtúllépése KB-onként</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Proxied" xml:space="preserve"> <trans-unit id="Proxied" xml:space="preserve">
@@ -5436,7 +5446,7 @@ Engedélyezze a „Beállítások -&gt; Hálózat és kiszolgálók” menüben.
</trans-unit> </trans-unit>
<trans-unit id="Received total" xml:space="preserve"> <trans-unit id="Received total" xml:space="preserve">
<source>Received total</source> <source>Received total</source>
<target>Összes fogadott</target> <target>Összes fogadott üzenet</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve">
@@ -5808,7 +5818,7 @@ Engedélyezze a „Beállítások -&gt; Hálózat és kiszolgálók” menüben.
</trans-unit> </trans-unit>
<trans-unit id="Save profile password" xml:space="preserve"> <trans-unit id="Save profile password" xml:space="preserve">
<source>Save profile password</source> <source>Save profile password</source>
<target>Felhasználói fiók jelszavának mentése</target> <target>Profiljelszó mentése</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Save servers" xml:space="preserve"> <trans-unit id="Save servers" xml:space="preserve">
@@ -6148,7 +6158,7 @@ Engedélyezze a „Beállítások -&gt; Hálózat és kiszolgálók” menüben.
</trans-unit> </trans-unit>
<trans-unit id="Sent total" xml:space="preserve"> <trans-unit id="Sent total" xml:space="preserve">
<source>Sent total</source> <source>Sent total</source>
<target>Összes elküldött</target> <target>Összes elküldött üzenet</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Sent via proxy" xml:space="preserve"> <trans-unit id="Sent via proxy" xml:space="preserve">
@@ -6363,7 +6373,7 @@ Engedélyezze a „Beállítások -&gt; Hálózat és kiszolgálók” menüben.
</trans-unit> </trans-unit>
<trans-unit id="Show message status" xml:space="preserve"> <trans-unit id="Show message status" xml:space="preserve">
<source>Show message status</source> <source>Show message status</source>
<target>Üzenet állapot megjelenítése</target> <target>Üzenetállapot megjelenítése</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Show percentage" xml:space="preserve"> <trans-unit id="Show percentage" xml:space="preserve">
@@ -6463,6 +6473,7 @@ Engedélyezze a „Beállítások -&gt; Hálózat és kiszolgálók” menüben.
</trans-unit> </trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source> <source>SimpleX protocols reviewed by Trail of Bits.</source>
<target>A SimpleX Chat biztonsága a Trail of Bits által lett újraauditálva.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve"> <trans-unit id="Simplified incognito mode" xml:space="preserve">
@@ -6642,10 +6653,12 @@ Engedélyezze a „Beállítások -&gt; Hálózat és kiszolgálók” menüben.
</trans-unit> </trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve"> <trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source> <source>Switch audio and video during the call.</source>
<target>Hang/Videó váltása hívás közben.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source> <source>Switch chat profile for 1-time invitations.</source>
<target>Csevegési profilváltás az egyszer használható meghívókhoz.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="System" xml:space="preserve"> <trans-unit id="System" xml:space="preserve">
@@ -6665,7 +6678,7 @@ Engedélyezze a „Beállítások -&gt; Hálózat és kiszolgálók” menüben.
</trans-unit> </trans-unit>
<trans-unit id="TCP connection timeout" xml:space="preserve"> <trans-unit id="TCP connection timeout" xml:space="preserve">
<source>TCP connection timeout</source> <source>TCP connection timeout</source>
<target>TCP kapcsolat időtúllépés</target> <target>TCP kapcsolat időtúllépése</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <trans-unit id="TCP_KEEPCNT" xml:space="preserve">
@@ -6700,7 +6713,7 @@ Engedélyezze a „Beállítások -&gt; Hálózat és kiszolgálók” menüben.
</trans-unit> </trans-unit>
<trans-unit id="Tap to Connect" xml:space="preserve"> <trans-unit id="Tap to Connect" xml:space="preserve">
<source>Tap to Connect</source> <source>Tap to Connect</source>
<target>Koppintson a kapcsolódáshoz</target> <target>Koppintson ide a kapcsolódáshoz</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Tap to activate profile." xml:space="preserve"> <trans-unit id="Tap to activate profile." xml:space="preserve">
@@ -6710,12 +6723,12 @@ Engedélyezze a „Beállítások -&gt; Hálózat és kiszolgálók” menüben.
</trans-unit> </trans-unit>
<trans-unit id="Tap to join" xml:space="preserve"> <trans-unit id="Tap to join" xml:space="preserve">
<source>Tap to join</source> <source>Tap to join</source>
<target>Koppintson a csatlakozáshoz</target> <target>Koppintson ide a csatlakozáshoz</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Tap to join incognito" xml:space="preserve"> <trans-unit id="Tap to join incognito" xml:space="preserve">
<source>Tap to join incognito</source> <source>Tap to join incognito</source>
<target>Koppintson az inkognitóban való csatlakozáshoz</target> <target>Koppintson ide az inkognitóban való csatlakozáshoz</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Tap to paste link" xml:space="preserve"> <trans-unit id="Tap to paste link" xml:space="preserve">
@@ -6725,7 +6738,7 @@ Engedélyezze a „Beállítások -&gt; Hálózat és kiszolgálók” menüben.
</trans-unit> </trans-unit>
<trans-unit id="Tap to scan" xml:space="preserve"> <trans-unit id="Tap to scan" xml:space="preserve">
<source>Tap to scan</source> <source>Tap to scan</source>
<target>Koppintson a beolvasáshoz</target> <target>Koppintson ide a QR-kód beolvasáshoz</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Temporary file error" xml:space="preserve"> <trans-unit id="Temporary file error" xml:space="preserve">
@@ -6770,7 +6783,7 @@ Engedélyezze a „Beállítások -&gt; Hálózat és kiszolgálók” menüben.
</trans-unit> </trans-unit>
<trans-unit id="The 1st platform without any user identifiers private by design." xml:space="preserve"> <trans-unit id="The 1st platform without any user identifiers private by design." xml:space="preserve">
<source>The 1st platform without any user identifiers private by design.</source> <source>The 1st platform without any user identifiers private by design.</source>
<target>Az első csevegési rendszer bármiféle felhasználó azonosító nélkül - privátra lett tervezre.</target> <target>Az első csevegési rendszer bármiféle felhasználó-azonosító nélkül - privátra lett tervezre.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="The ID of the next message is incorrect (less or equal to the previous).&#10;It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous).&#10;It can happen because of some bug or when the connection is compromised." xml:space="preserve">
@@ -6992,7 +7005,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő.</target>
</trans-unit> </trans-unit>
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
<target>Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználói azonosítók helyett, a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, minden egyes ismerőshöz egy-egy különbözőt.</target> <target>Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználó-azonosítók helyett, a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, minden egyes ismerőshöz egy-egy különbözőt.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
@@ -7059,12 +7072,12 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll
</trans-unit> </trans-unit>
<trans-unit id="Total" xml:space="preserve"> <trans-unit id="Total" xml:space="preserve">
<source>Total</source> <source>Total</source>
<target>Összesen</target> <target>Összes kapcsolat</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Transport isolation" xml:space="preserve"> <trans-unit id="Transport isolation" xml:space="preserve">
<source>Transport isolation</source> <source>Transport isolation</source>
<target>Kapcsolat izolációs mód</target> <target>Átvitel-izoláció módja</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Transport sessions" xml:space="preserve"> <trans-unit id="Transport sessions" xml:space="preserve">
@@ -7728,7 +7741,7 @@ Csatlakozáskérés megismétlése?</target>
</trans-unit> </trans-unit>
<trans-unit id="You can enable them later via app Privacy &amp; Security settings." xml:space="preserve"> <trans-unit id="You can enable them later via app Privacy &amp; Security settings." xml:space="preserve">
<source>You can enable them later via app Privacy &amp; Security settings.</source> <source>You can enable them later via app Privacy &amp; Security settings.</source>
<target>Később engedélyezheti őket az alkalmazás „Adatvédelem és biztonság” menüben.</target> <target>Később engedélyezheti őket az alkalmazás „Adatvédelem és biztonság” menüben.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You can give another try." xml:space="preserve"> <trans-unit id="You can give another try." xml:space="preserve">
@@ -7738,7 +7751,7 @@ Csatlakozáskérés megismétlése?</target>
</trans-unit> </trans-unit>
<trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve">
<source>You can hide or mute a user profile - swipe it to the right.</source> <source>You can hide or mute a user profile - swipe it to the right.</source>
<target>Elrejtheti vagy lenémíthatja a felhasználó profiljait - csúsztassa jobbra a profilt.</target> <target>Elrejtheti vagy lenémíthatja a felhasználó -profiljait - csúsztassa jobbra a profilt.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve"> <trans-unit id="You can make it visible to your SimpleX contacts via Settings." xml:space="preserve">
@@ -9068,7 +9081,7 @@ utoljára fogadott üzenet: %2$@</target>
</trans-unit> </trans-unit>
<trans-unit id="NSCameraUsageDescription" xml:space="preserve"> <trans-unit id="NSCameraUsageDescription" xml:space="preserve">
<source>SimpleX needs camera access to scan QR codes to connect to other users and for video calls.</source> <source>SimpleX needs camera access to scan QR codes to connect to other users and for video calls.</source>
<target>A SimpleX-nek kamera-hozzáférésre van szüksége a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz.</target> <target>A SimpleXnek kamera-hozzáférésre van szüksége a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz.</target>
<note>Privacy - Camera Usage Description</note> <note>Privacy - Camera Usage Description</note>
</trans-unit> </trans-unit>
<trans-unit id="NSFaceIDUsageDescription" xml:space="preserve"> <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve">
@@ -9083,12 +9096,12 @@ utoljára fogadott üzenet: %2$@</target>
</trans-unit> </trans-unit>
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
<source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source>
<target>A SimpleX-nek mikrofon-hozzáférésre van szüksége hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez.</target> <target>A SimpleXnek mikrofon-hozzáférésre van szüksége hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez.</target>
<note>Privacy - Microphone Usage Description</note> <note>Privacy - Microphone Usage Description</note>
</trans-unit> </trans-unit>
<trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve"> <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve">
<source>SimpleX needs access to Photo Library for saving captured and received media</source> <source>SimpleX needs access to Photo Library for saving captured and received media</source>
<target>A SimpleX-nek hozzáférésre van szüksége a Galériához a rögzített és fogadott média mentéséhez</target> <target>A SimpleXnek galéria-hozzáférésre van szüksége a rögzített és fogadott média mentéséhez</target>
<note>Privacy - Photo Library Additions Usage Description</note> <note>Privacy - Photo Library Additions Usage Description</note>
</trans-unit> </trans-unit>
</body> </body>
@@ -1087,6 +1087,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better calls" xml:space="preserve"> <trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source> <source>Better calls</source>
<target>Chiamate migliorate</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better groups" xml:space="preserve"> <trans-unit id="Better groups" xml:space="preserve">
@@ -1096,6 +1097,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better message dates." xml:space="preserve"> <trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source> <source>Better message dates.</source>
<target>Date dei messaggi migliorate.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better messages" xml:space="preserve"> <trans-unit id="Better messages" xml:space="preserve">
@@ -1110,14 +1112,17 @@
</trans-unit> </trans-unit>
<trans-unit id="Better notifications" xml:space="preserve"> <trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source> <source>Better notifications</source>
<target>Notifiche migliorate</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve"> <trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source> <source>Better security ✅</source>
<target>Sicurezza migliorata ✅</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better user experience" xml:space="preserve"> <trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source> <source>Better user experience</source>
<target>Esperienza utente migliorata</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Black" xml:space="preserve"> <trans-unit id="Black" xml:space="preserve">
@@ -1942,6 +1947,7 @@ Questo è il tuo link una tantum!</target>
</trans-unit> </trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve"> <trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source> <source>Customizable message shape.</source>
<target>Forma dei messaggi personalizzabile.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Customize theme" xml:space="preserve"> <trans-unit id="Customize theme" xml:space="preserve">
@@ -2240,6 +2246,7 @@ Questo è il tuo link una tantum!</target>
</trans-unit> </trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source> <source>Delete or moderate up to 200 messages.</source>
<target>Elimina o modera fino a 200 messaggi.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve"> <trans-unit id="Delete pending connection?" xml:space="preserve">
@@ -3367,6 +3374,7 @@ Questo è il tuo link una tantum!</target>
</trans-unit> </trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> <trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source> <source>Forward up to 20 messages at once.</source>
<target>Inoltra fino a 20 messaggi alla volta.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Forwarded" xml:space="preserve"> <trans-unit id="Forwarded" xml:space="preserve">
@@ -3761,6 +3769,8 @@ Errore: %2$@</target>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve"> <trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage. <source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source> More improvements are coming soon!</source>
<target>Consegna migliorata, utilizzo di traffico ridotto.
Altri miglioramenti sono in arrivo!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve"> <trans-unit id="Improved message delivery" xml:space="preserve">
@@ -6463,6 +6473,7 @@ Attivalo nelle impostazioni *Rete e server*.</target>
</trans-unit> </trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source> <source>SimpleX protocols reviewed by Trail of Bits.</source>
<target>Protocolli di SimpleX esaminati da Trail of Bits.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve"> <trans-unit id="Simplified incognito mode" xml:space="preserve">
@@ -6642,10 +6653,12 @@ Attivalo nelle impostazioni *Rete e server*.</target>
</trans-unit> </trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve"> <trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source> <source>Switch audio and video during the call.</source>
<target>Cambia tra audio e video durante la chiamata.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source> <source>Switch chat profile for 1-time invitations.</source>
<target>Cambia profilo di chat per inviti una tantum.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="System" xml:space="preserve"> <trans-unit id="System" xml:space="preserve">
@@ -4121,10 +4121,12 @@ This is your link for group %@!</source>
</trans-unit> </trans-unit>
<trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> <trans-unit id="Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve">
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> <source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
<target>メッセージ、ファイル、通話は、前方秘匿性、否認可能性および侵入復元性を備えた**エンドツーエンドの暗号化**によって保護されます。</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve"> <trans-unit id="Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." xml:space="preserve">
<source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source> <source>Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
<target>メッセージ、ファイル、通話は、前方秘匿性、否認可能性および侵入復元性を備えた**耐量子E2E暗号化**によって保護されます。</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Migrate device" xml:space="preserve"> <trans-unit id="Migrate device" xml:space="preserve">
@@ -1087,6 +1087,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better calls" xml:space="preserve"> <trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source> <source>Better calls</source>
<target>Betere gesprekken</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better groups" xml:space="preserve"> <trans-unit id="Better groups" xml:space="preserve">
@@ -1096,6 +1097,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better message dates." xml:space="preserve"> <trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source> <source>Better message dates.</source>
<target>Betere datums voor berichten.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better messages" xml:space="preserve"> <trans-unit id="Better messages" xml:space="preserve">
@@ -1110,14 +1112,17 @@
</trans-unit> </trans-unit>
<trans-unit id="Better notifications" xml:space="preserve"> <trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source> <source>Better notifications</source>
<target>Betere meldingen</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve"> <trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source> <source>Better security ✅</source>
<target>Betere beveiliging ✅</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better user experience" xml:space="preserve"> <trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source> <source>Better user experience</source>
<target>Betere gebruikerservaring</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Black" xml:space="preserve"> <trans-unit id="Black" xml:space="preserve">
@@ -1942,6 +1947,7 @@ Dit is uw eigen eenmalige link!</target>
</trans-unit> </trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve"> <trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source> <source>Customizable message shape.</source>
<target>Aanpasbare berichtvorm.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Customize theme" xml:space="preserve"> <trans-unit id="Customize theme" xml:space="preserve">
@@ -2240,6 +2246,7 @@ Dit is uw eigen eenmalige link!</target>
</trans-unit> </trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source> <source>Delete or moderate up to 200 messages.</source>
<target>Maximaal 200 berichten verwijderen of modereren.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve"> <trans-unit id="Delete pending connection?" xml:space="preserve">
@@ -3367,6 +3374,7 @@ Dit is uw eigen eenmalige link!</target>
</trans-unit> </trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> <trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source> <source>Forward up to 20 messages at once.</source>
<target>Stuur maximaal 20 berichten tegelijk door.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Forwarded" xml:space="preserve"> <trans-unit id="Forwarded" xml:space="preserve">
@@ -3761,6 +3769,8 @@ Fout: %2$@</target>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve"> <trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage. <source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source> More improvements are coming soon!</source>
<target>Verbeterde levering, minder data gebruik.
Binnenkort meer verbeteringen!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve"> <trans-unit id="Improved message delivery" xml:space="preserve">
@@ -6463,6 +6473,7 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
</trans-unit> </trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source> <source>SimpleX protocols reviewed by Trail of Bits.</source>
<target>SimpleX-protocollen beoordeeld door Trail of Bits.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve"> <trans-unit id="Simplified incognito mode" xml:space="preserve">
@@ -6642,10 +6653,12 @@ Schakel dit in in *Netwerk en servers*-instellingen.</target>
</trans-unit> </trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve"> <trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source> <source>Switch audio and video during the call.</source>
<target>Wisselen tussen audio en video tijdens het gesprek.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source> <source>Switch chat profile for 1-time invitations.</source>
<target>Wijzig chatprofiel voor eenmalige uitnodigingen.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="System" xml:space="preserve"> <trans-unit id="System" xml:space="preserve">
@@ -139,6 +139,7 @@
</trans-unit> </trans-unit>
<trans-unit id="%@, %@" xml:space="preserve"> <trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source> <source>%1$@, %2$@</source>
<target>%1$@, %2$@</target>
<note>format for date separator in chat</note> <note>format for date separator in chat</note>
</trans-unit> </trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve"> <trans-unit id="%@, %@ and %lld members" xml:space="preserve">
@@ -1086,6 +1087,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better calls" xml:space="preserve"> <trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source> <source>Better calls</source>
<target>Улучшенные звонки</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better groups" xml:space="preserve"> <trans-unit id="Better groups" xml:space="preserve">
@@ -1095,6 +1097,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better message dates." xml:space="preserve"> <trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source> <source>Better message dates.</source>
<target>Улучшенные даты сообщений.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better messages" xml:space="preserve"> <trans-unit id="Better messages" xml:space="preserve">
@@ -1109,14 +1112,17 @@
</trans-unit> </trans-unit>
<trans-unit id="Better notifications" xml:space="preserve"> <trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source> <source>Better notifications</source>
<target>Улучшенные уведомления</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve"> <trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source> <source>Better security ✅</source>
<target>Улучшенная безопасность ✅</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better user experience" xml:space="preserve"> <trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source> <source>Better user experience</source>
<target>Улучшенный интерфейс</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Black" xml:space="preserve"> <trans-unit id="Black" xml:space="preserve">
@@ -1941,6 +1947,7 @@ This is your own one-time link!</source>
</trans-unit> </trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve"> <trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source> <source>Customizable message shape.</source>
<target>Настраиваемая форма сообщений.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Customize theme" xml:space="preserve"> <trans-unit id="Customize theme" xml:space="preserve">
@@ -2239,6 +2246,7 @@ This is your own one-time link!</source>
</trans-unit> </trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source> <source>Delete or moderate up to 200 messages.</source>
<target>Удаляйте или модерируйте до 200 сообщений.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve"> <trans-unit id="Delete pending connection?" xml:space="preserve">
@@ -3366,6 +3374,7 @@ This is your own one-time link!</source>
</trans-unit> </trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> <trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source> <source>Forward up to 20 messages at once.</source>
<target>Пересылайте до 20 сообщений за раз.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Forwarded" xml:space="preserve"> <trans-unit id="Forwarded" xml:space="preserve">
@@ -3760,6 +3769,7 @@ Error: %2$@</source>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve"> <trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage. <source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source> More improvements are coming soon!</source>
<target>Улучшенная доставка, меньше трафик.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve"> <trans-unit id="Improved message delivery" xml:space="preserve">
@@ -6462,6 +6472,7 @@ Enable in *Network &amp; servers* settings.</source>
</trans-unit> </trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source> <source>SimpleX protocols reviewed by Trail of Bits.</source>
<target>Аудит SimpleX протоколов от Trail of Bits.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve"> <trans-unit id="Simplified incognito mode" xml:space="preserve">
@@ -6641,10 +6652,12 @@ Enable in *Network &amp; servers* settings.</source>
</trans-unit> </trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve"> <trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source> <source>Switch audio and video during the call.</source>
<target>Переключайте звук и видео во время звонка.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source> <source>Switch chat profile for 1-time invitations.</source>
<target>Переключайте профиль чата для одноразовых приглашений.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="System" xml:space="preserve"> <trans-unit id="System" xml:space="preserve">
@@ -1087,6 +1087,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better calls" xml:space="preserve"> <trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source> <source>Better calls</source>
<target>Daha iyi aramalar</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better groups" xml:space="preserve"> <trans-unit id="Better groups" xml:space="preserve">
@@ -1096,6 +1097,7 @@
</trans-unit> </trans-unit>
<trans-unit id="Better message dates." xml:space="preserve"> <trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source> <source>Better message dates.</source>
<target>Daha iyi mesaj tarihleri.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better messages" xml:space="preserve"> <trans-unit id="Better messages" xml:space="preserve">
@@ -1110,14 +1112,17 @@
</trans-unit> </trans-unit>
<trans-unit id="Better notifications" xml:space="preserve"> <trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source> <source>Better notifications</source>
<target>Daha iyi bildirimler</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve"> <trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source> <source>Better security ✅</source>
<target>Daha iyi güvenlik ✅</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Better user experience" xml:space="preserve"> <trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source> <source>Better user experience</source>
<target>Daha iyi kullanıcı deneyimi</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Black" xml:space="preserve"> <trans-unit id="Black" xml:space="preserve">
@@ -1942,6 +1947,7 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
</trans-unit> </trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve"> <trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source> <source>Customizable message shape.</source>
<target>Özelleştirilebilir mesaj şekli.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Customize theme" xml:space="preserve"> <trans-unit id="Customize theme" xml:space="preserve">
@@ -2240,6 +2246,7 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
</trans-unit> </trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve"> <trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source> <source>Delete or moderate up to 200 messages.</source>
<target>200'e kadar mesajı silin veya düzenleyin.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve"> <trans-unit id="Delete pending connection?" xml:space="preserve">
@@ -3367,6 +3374,7 @@ Bu senin kendi tek kullanımlık bağlantın!</target>
</trans-unit> </trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve"> <trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source> <source>Forward up to 20 messages at once.</source>
<target>Aynı anda en fazla 20 mesaj iletin.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Forwarded" xml:space="preserve"> <trans-unit id="Forwarded" xml:space="preserve">
@@ -3761,6 +3769,8 @@ Hata: %2$@</target>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve"> <trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage. <source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source> More improvements are coming soon!</source>
<target>İyileştirilmiş teslimat, azaltılmış trafik kullanımı.
Daha fazla iyileştirme yakında geliyor!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve"> <trans-unit id="Improved message delivery" xml:space="preserve">
@@ -6463,6 +6473,7 @@ Enable in *Network &amp; servers* settings.</source>
</trans-unit> </trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve"> <trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source> <source>SimpleX protocols reviewed by Trail of Bits.</source>
<target>SimpleX protokolleri Trail of Bits tarafından incelenmiştir.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve"> <trans-unit id="Simplified incognito mode" xml:space="preserve">
@@ -6642,10 +6653,12 @@ Enable in *Network &amp; servers* settings.</source>
</trans-unit> </trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve"> <trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source> <source>Switch audio and video during the call.</source>
<target>Görüşme sırasında ses ve görüntüyü değiştirin.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve"> <trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source> <source>Switch chat profile for 1-time invitations.</source>
<target>Sohbet profilini 1 kerelik davetler için değiştirin.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="System" xml:space="preserve"> <trans-unit id="System" xml:space="preserve">
+348 -128
View File
@@ -26,14 +26,52 @@ enum NSENotification {
case nse(UNMutableNotificationContent) case nse(UNMutableNotificationContent)
case callkit(RcvCallInvitation) case callkit(RcvCallInvitation)
case empty case empty
case msgInfo(NtfMsgAckInfo) }
var isCallInvitation: Bool { public enum NSENotificationData {
case connectionEvent(_ user: User, _ connEntity: ConnectionEntity)
case contactConnected(_ user: any UserLike, _ contact: Contact)
case contactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest)
case messageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem)
case callInvitation(_ invitation: RcvCallInvitation)
case msgInfo(NtfMsgAckInfo)
case noNtf
var callInvitation: RcvCallInvitation? {
switch self { switch self {
case let .nse(ntf): ntf.categoryIdentifier == ntfCategoryCallInvitation case let .callInvitation(invitation): invitation
case .callkit: true default: nil
case .empty: false }
case .msgInfo: false }
func notificationContent(_ badgeCount: Int) -> UNMutableNotificationContent {
return switch self {
case let .connectionEvent(user, connEntity): createConnectionEventNtf(user, connEntity, badgeCount)
case let .contactConnected(user, contact): createContactConnectedNtf(user, contact, badgeCount)
case let .contactRequest(user, contactRequest): createContactRequestNtf(user, contactRequest, badgeCount)
case let .messageReceived(user, cInfo, cItem): createMessageReceivedNtf(user, cInfo, cItem, badgeCount)
case let .callInvitation(invitation): createCallInvitationNtf(invitation, badgeCount)
case .msgInfo: UNMutableNotificationContent()
case .noNtf: UNMutableNotificationContent()
}
}
var notificationEvent: NSENotificationData? {
return switch self {
case .connectionEvent: self
case .contactConnected: self
case .contactRequest: self
case .messageReceived: self
case .callInvitation: self
case .msgInfo: nil
case .noNtf: nil
}
}
var newMsgData: (any UserLike, ChatInfo)? {
return switch self {
case let .messageReceived(user, cInfo, _): (user, cInfo)
default: nil
} }
} }
} }
@@ -43,9 +81,10 @@ enum NSENotification {
// or when background notification is received. // or when background notification is received.
class NSEThreads { class NSEThreads {
static let shared = NSEThreads() static let shared = NSEThreads()
private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock") static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock")
private var allThreads: Set<UUID> = [] private var allThreads: Set<UUID> = []
private var activeThreads: [(UUID, NotificationService)] = [] var activeThreads: [(UUID, NotificationService)] = []
var droppedNotifications: [(ChatId, NSENotificationData)] = []
func newThread() -> UUID { func newThread() -> UUID {
NSEThreads.queue.sync { NSEThreads.queue.sync {
@@ -64,22 +103,19 @@ class NSEThreads {
} }
} }
func processNotification(_ id: ChatId, _ ntf: NSENotification) async -> Void { func processNotification(_ id: ChatId, _ ntf: NSENotificationData) async -> Void {
var waitTime: Int64 = 5_000_000000
while waitTime > 0 {
if let (_, nse) = rcvEntityThread(id), if let (_, nse) = rcvEntityThread(id),
nse.shouldProcessNtf && nse.processReceivedNtf(ntf) { nse.expectedMessages[id]?.shouldProcessNtf ?? false {
break nse.processReceivedNtf(id, ntf, signalReady: true)
} else {
try? await Task.sleep(nanoseconds: 10_000000)
waitTime -= 10_000000
}
} }
} }
private func rcvEntityThread(_ id: ChatId) -> (UUID, NotificationService)? { private func rcvEntityThread(_ id: ChatId) -> (UUID, NotificationService)? {
NSEThreads.queue.sync { NSEThreads.queue.sync {
activeThreads.first(where: { (_, nse) in nse.receiveEntityId == id }) // this selects the earliest thread that:
// 1) has this connection in nse.expectedMessages
// 2) has not completed processing messages for this connection (not ready)
activeThreads.first(where: { (_, nse) in nse.expectedMessages[id]?.ready == false })
} }
} }
@@ -106,31 +142,38 @@ class NSEThreads {
} }
} }
struct ExpectedMessage {
var ntfConn: UserNtfConn
var receiveConnId: String?
var expectedMsgId: String?
var allowedGetNextAttempts: Int
var msgBestAttemptNtf: NSENotificationData?
var ready: Bool
var shouldProcessNtf: Bool
var startedProcessingNewMsgs: Bool
var semaphore: DispatchSemaphore
}
// Notification service extension creates a new instance of the class and calls didReceive for each notification. // Notification service extension creates a new instance of the class and calls didReceive for each notification.
// Each didReceive is called in its own thread, but multiple calls can be made in one process, and, empirically, there is never // Each didReceive is called in its own thread, but multiple calls can be made in one process, and, empirically, there is never
// more than one process of notification service extension exists at a time. // more than one process of notification service extension exists at a time.
// Soon after notification service delivers the last notification it is either suspended or terminated. // Soon after notification service delivers the last notification it is either suspended or terminated.
class NotificationService: UNNotificationServiceExtension { class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)? var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptNtf: NSENotification? // served as notification if no message attempts (msgBestAttemptNtf) could be produced
var serviceBestAttemptNtf: NSENotification?
var badgeCount: Int = 0 var badgeCount: Int = 0
// thread is added to allThreads here - if thread did not start chat, // thread is added to allThreads here - if thread did not start chat,
// chat does not need to be suspended but NSE state still needs to be set to "suspended". // chat does not need to be suspended but NSE state still needs to be set to "suspended".
var threadId: UUID? = NSEThreads.shared.newThread() var threadId: UUID? = NSEThreads.shared.newThread()
var notificationInfo: NtfMessages? var expectedMessages: Dictionary<String, ExpectedMessage> = [:] // key is receiveEntityId
var receiveEntityId: String?
var receiveConnId: String?
var expectedMessage: String?
var allowedGetNextAttempts: Int = 3
// return true if the message is taken - it prevents sending it to another NotificationService instance for processing
var shouldProcessNtf = false
var appSubscriber: AppSubscriber? var appSubscriber: AppSubscriber?
var returnedSuspension = false var returnedSuspension = false
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
logger.debug("DEBUGGING: NotificationService.didReceive") logger.debug("DEBUGGING: NotificationService.didReceive")
let ntf = if let ntf_ = request.content.mutableCopy() as? UNMutableNotificationContent { ntf_ } else { UNMutableNotificationContent() } let ntf = if let ntf_ = request.content.mutableCopy() as? UNMutableNotificationContent { ntf_ } else { UNMutableNotificationContent() }
setBestAttemptNtf(ntf) setServiceBestAttemptNtf(ntf)
self.contentHandler = contentHandler self.contentHandler = contentHandler
registerGroupDefaults() registerGroupDefaults()
let appState = appStateGroupDefault.get() let appState = appStateGroupDefault.get()
@@ -138,13 +181,11 @@ class NotificationService: UNNotificationServiceExtension {
switch appState { switch appState {
case .stopped: case .stopped:
setBadgeCount() setBadgeCount()
setBestAttemptNtf(createAppStoppedNtf()) setServiceBestAttemptNtf(createAppStoppedNtf(badgeCount))
deliverBestAttemptNtf() deliverBestAttemptNtf()
case .suspended: case .suspended:
setBadgeCount()
receiveNtfMessages(request, contentHandler) receiveNtfMessages(request, contentHandler)
case .suspending: case .suspending:
setBadgeCount()
Task { Task {
let state: AppState = await withCheckedContinuation { cont in let state: AppState = await withCheckedContinuation { cont in
appSubscriber = appStateSubscriber { s in appSubscriber = appStateSubscriber { s in
@@ -171,8 +212,9 @@ class NotificationService: UNNotificationServiceExtension {
deliverBestAttemptNtf() deliverBestAttemptNtf()
} }
} }
default: case .active: contentHandler(UNMutableNotificationContent())
deliverBestAttemptNtf() case .activating: contentHandler(UNMutableNotificationContent())
case .bgRefresh: contentHandler(UNMutableNotificationContent())
} }
} }
@@ -192,78 +234,165 @@ class NotificationService: UNNotificationServiceExtension {
if let t = threadId { NSEThreads.shared.startThread(t, self) } if let t = threadId { NSEThreads.shared.startThread(t, self) }
let dbStatus = startChat() let dbStatus = startChat()
if case .ok = dbStatus, if case .ok = dbStatus,
let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { let ntfConns = apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo) {
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.receivedMsg_ == nil ? 0 : 1))") logger.debug("NotificationService: receiveNtfMessages: apiGetNtfConns ntfConns count = \(ntfConns.count)")
if let connEntity = ntfInfo.connEntity_ {
setBestAttemptNtf( for ntfConn in ntfConns {
ntfInfo.ntfsEnabled addExpectedMessage(ntfConn: ntfConn)
? .nse(createConnectionEventNtf(ntfInfo.user, connEntity)) }
: .empty
) let connIdsToGet = expectedMessages.compactMap { (id, _) in
if let id = connEntity.id, ntfInfo.expectedMsg_ != nil { let started = NSEThreads.queue.sync {
notificationInfo = ntfInfo let canStart = checkCanStart(id)
receiveEntityId = id if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: can start: \(canStart)") }
receiveConnId = connEntity.conn.agentConnId if canStart {
let expectedMsgId = ntfInfo.expectedMsg_?.msgId processDroppedNotifications(id)
let receivedMsgId = ntfInfo.receivedMsg_?.msgId expectedMessages[id]?.startedProcessingNewMsgs = true
logger.debug("NotificationService: receiveNtfMessages: expectedMsgId = \(expectedMsgId ?? "nil", privacy: .private), receivedMsgId = \(receivedMsgId ?? "nil", privacy: .private)") expectedMessages[id]?.shouldProcessNtf = true
expectedMessage = expectedMsgId }
shouldProcessNtf = true return canStart
}
if started {
return expectedMessages[id]?.receiveConnId
} else {
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: entity \(id, privacy: .private) waiting on semaphore") }
expectedMessages[id]?.semaphore.wait()
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): receiveNtfMessages: entity \(id, privacy: .private) proceeding after semaphore") }
Task {
NSEThreads.queue.sync {
processDroppedNotifications(id)
expectedMessages[id]?.startedProcessingNewMsgs = true
expectedMessages[id]?.shouldProcessNtf = true
}
if let connId = expectedMessages[id]?.receiveConnId {
let _ = getConnNtfMessage(connId: connId)
}
}
return nil
}
}
if !connIdsToGet.isEmpty {
if let r = apiGetConnNtfMessages(connIds: connIdsToGet) {
logger.debug("NotificationService: receiveNtfMessages: apiGetConnNtfMessages count = \(r.count)")
}
return return
} }
}
} else if let dbStatus = dbStatus { } else if let dbStatus = dbStatus {
setBestAttemptNtf(createErrorNtf(dbStatus)) setServiceBestAttemptNtf(createErrorNtf(dbStatus, badgeCount))
} }
} }
deliverBestAttemptNtf() deliverBestAttemptNtf()
} }
func addExpectedMessage(ntfConn: UserNtfConn) {
if let connEntity = ntfConn.connEntity_,
let receiveEntityId = connEntity.id, ntfConn.expectedMsg_ != nil {
let expectedMsgId = ntfConn.expectedMsg_?.msgId
logger.debug("NotificationService: addExpectedMessage: expectedMsgId = \(expectedMsgId ?? "nil", privacy: .private)")
expectedMessages[receiveEntityId] = ExpectedMessage(
ntfConn: ntfConn,
receiveConnId: connEntity.conn.agentConnId,
expectedMsgId: expectedMsgId,
allowedGetNextAttempts: 3,
msgBestAttemptNtf: ntfConn.defaultBestAttemptNtf,
ready: false,
shouldProcessNtf: false,
startedProcessingNewMsgs: false,
semaphore: DispatchSemaphore(value: 0)
)
}
}
func checkCanStart(_ entityId: String) -> Bool {
return !NSEThreads.shared.activeThreads.contains(where: {
(tId, nse) in tId != threadId && nse.expectedMessages.contains(where: { $0.key == entityId })
})
}
func processDroppedNotifications(_ entityId: String) {
if !NSEThreads.shared.droppedNotifications.isEmpty {
let messagesToProcess = NSEThreads.shared.droppedNotifications.filter { (eId, _) in eId == entityId }
NSEThreads.shared.droppedNotifications.removeAll(where: { (eId, _) in eId == entityId })
for (index, (_, ntf)) in messagesToProcess.enumerated() {
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entity \(entityId, privacy: .private): processing dropped notification \(index, privacy: .private)") }
processReceivedNtf(entityId, ntf, signalReady: false)
}
}
}
override func serviceExtensionTimeWillExpire() { override func serviceExtensionTimeWillExpire() {
logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire") logger.debug("DEBUGGING: NotificationService.serviceExtensionTimeWillExpire")
deliverBestAttemptNtf(urgent: true) deliverBestAttemptNtf(urgent: true)
} }
func processReceivedNtf(_ ntf: NSENotification) -> Bool { var expectingMoreMessages: Bool {
guard let ntfInfo = notificationInfo, let expectedMsgTs = ntfInfo.expectedMsg_?.msgTs else { return false } !expectedMessages.allSatisfy { $0.value.ready }
if !ntfInfo.user.showNotifications { }
self.setBestAttemptNtf(.empty)
func processReceivedNtf(_ id: ChatId, _ ntf: NSENotificationData, signalReady: Bool) {
guard let expectedMessage = expectedMessages[id] else {
return
}
guard let expectedMsgTs = expectedMessage.ntfConn.expectedMsg_?.msgTs else {
NSEThreads.shared.droppedNotifications.append((id, ntf))
if signalReady { entityReady(id) }
return
} }
if case let .msgInfo(info) = ntf { if case let .msgInfo(info) = ntf {
if info.msgId == expectedMessage { if info.msgId == expectedMessage.expectedMsgId {
expectedMessage = nil
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): expected") logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): expected")
expectedMessages[id]?.expectedMsgId = nil
if signalReady { entityReady(id) }
self.deliverBestAttemptNtf() self.deliverBestAttemptNtf()
return true
} else if let msgTs = info.msgTs_, msgTs > expectedMsgTs { } else if let msgTs = info.msgTs_, msgTs > expectedMsgTs {
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, let other instance to process it, stopping this one") logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, let other instance to process it, stopping this one")
NSEThreads.shared.droppedNotifications.append((id, ntf))
if signalReady { entityReady(id) }
self.deliverBestAttemptNtf() self.deliverBestAttemptNtf()
return false } else if (expectedMessages[id]?.allowedGetNextAttempts ?? 0) > 0, let receiveConnId = expectedMessages[id]?.receiveConnId {
} else if allowedGetNextAttempts > 0, let receiveConnId = receiveConnId {
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, get next message") logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unexpected msgInfo, get next message")
allowedGetNextAttempts -= 1 expectedMessages[id]?.allowedGetNextAttempts -= 1
if let receivedMsg = apiGetConnNtfMessage(connId: receiveConnId) { if let receivedMsg = getConnNtfMessage(connId: receiveConnId) {
logger.debug("NotificationService processNtf, on apiGetConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private), receivedMsg msgId = \(receivedMsg.msgId, privacy: .private)") logger.debug("NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private), receivedMsg msgId = \(receivedMsg.msgId, privacy: .private)")
return true
} else { } else {
logger.debug("NotificationService processNtf, on apiGetConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private): no next message, deliver best attempt") logger.debug("NotificationService processNtf, on getConnNtfMessage: msgInfo msgId = \(info.msgId, privacy: .private): no next message, deliver best attempt")
NSEThreads.shared.droppedNotifications.append((id, ntf))
if signalReady { entityReady(id) }
self.deliverBestAttemptNtf() self.deliverBestAttemptNtf()
return false
} }
} else { } else {
logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unknown message, let other instance to process it") logger.debug("NotificationService processNtf: msgInfo msgId = \(info.msgId, privacy: .private): unknown message, let other instance to process it")
NSEThreads.shared.droppedNotifications.append((id, ntf))
if signalReady { entityReady(id) }
self.deliverBestAttemptNtf() self.deliverBestAttemptNtf()
return false
} }
} else if ntfInfo.user.showNotifications { } else if expectedMessage.ntfConn.user.showNotifications {
logger.debug("NotificationService processNtf: setting best attempt") logger.debug("NotificationService processNtf: setting best attempt")
self.setBestAttemptNtf(ntf) if ntf.notificationEvent != nil {
if ntf.isCallInvitation { setBadgeCount()
self.deliverBestAttemptNtf()
} }
return true let prevBestAttempt = expectedMessages[id]?.msgBestAttemptNtf
if prevBestAttempt?.callInvitation != nil {
if ntf.callInvitation != nil { // replace with newer call
expectedMessages[id]?.msgBestAttemptNtf = ntf
} // otherwise keep call as best attempt
} else {
expectedMessages[id]?.msgBestAttemptNtf = ntf
}
} else {
NSEThreads.shared.droppedNotifications.append((id, ntf))
if signalReady { entityReady(id) }
}
}
func entityReady(_ entityId: ChatId) {
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entityReady: entity \(entityId, privacy: .private)") }
expectedMessages[entityId]?.ready = true
if let (tNext, nse) = NSEThreads.shared.activeThreads.first(where: { (_, nse) in nse.expectedMessages[entityId]?.startedProcessingNewMsgs == false }) {
if let t = threadId { logger.debug("NotificationService thread \(t, privacy: .private): entityReady: signal next thread \(tNext, privacy: .private) for entity \(entityId, privacy: .private)") }
nse.expectedMessages[entityId]?.semaphore.signal()
} }
return false
} }
func setBadgeCount() { func setBadgeCount() {
@@ -271,24 +400,18 @@ class NotificationService: UNNotificationServiceExtension {
ntfBadgeCountGroupDefault.set(badgeCount) ntfBadgeCountGroupDefault.set(badgeCount)
} }
func setBestAttemptNtf(_ ntf: UNMutableNotificationContent) { func setServiceBestAttemptNtf(_ ntf: UNMutableNotificationContent) {
setBestAttemptNtf(.nse(ntf)) logger.debug("NotificationService.setServiceBestAttemptNtf")
} serviceBestAttemptNtf = .nse(ntf)
func setBestAttemptNtf(_ ntf: NSENotification) {
logger.debug("NotificationService.setBestAttemptNtf")
if case let .nse(notification) = ntf {
notification.badge = badgeCount as NSNumber
bestAttemptNtf = .nse(notification)
} else {
bestAttemptNtf = ntf
}
} }
private func deliverBestAttemptNtf(urgent: Bool = false) { private func deliverBestAttemptNtf(urgent: Bool = false) {
if (urgent || !expectingMoreMessages) {
logger.debug("NotificationService.deliverBestAttemptNtf") logger.debug("NotificationService.deliverBestAttemptNtf")
// stop processing other messages // stop processing other messages
shouldProcessNtf = false for (key, _) in expectedMessages {
expectedMessages[key]?.shouldProcessNtf = false
}
let suspend: Bool let suspend: Bool
if let t = threadId { if let t = threadId {
@@ -299,9 +422,10 @@ class NotificationService: UNNotificationServiceExtension {
} }
deliverCallkitOrNotification(urgent: urgent, suspend: suspend) deliverCallkitOrNotification(urgent: urgent, suspend: suspend)
} }
}
private func deliverCallkitOrNotification(urgent: Bool, suspend: Bool = false) { private func deliverCallkitOrNotification(urgent: Bool, suspend: Bool = false) {
if case .callkit = bestAttemptNtf { if useCallKit() && expectedMessages.contains(where: { $0.value.msgBestAttemptNtf?.callInvitation != nil }) {
logger.debug("NotificationService.deliverCallkitOrNotification: will suspend, callkit") logger.debug("NotificationService.deliverCallkitOrNotification: will suspend, callkit")
if urgent { if urgent {
// suspending NSE even though there may be other notifications // suspending NSE even though there may be other notifications
@@ -339,19 +463,13 @@ class NotificationService: UNNotificationServiceExtension {
} }
private func deliverNotification() { private func deliverNotification() {
if let handler = contentHandler, let ntf = bestAttemptNtf { if let handler = contentHandler, let ntf = prepareNotification() {
contentHandler = nil contentHandler = nil
bestAttemptNtf = nil serviceBestAttemptNtf = nil
let deliver: (UNMutableNotificationContent?) -> Void = { ntf in
let useNtf = if let ntf = ntf {
appStateGroupDefault.get().running ? UNMutableNotificationContent() : ntf
} else {
UNMutableNotificationContent()
}
handler(useNtf)
}
switch ntf { switch ntf {
case let .nse(content): deliver(content) case let .nse(content):
content.badge = badgeCount as NSNumber
handler(content)
case let .callkit(invitation): case let .callkit(invitation):
logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)") logger.debug("NotificationService reportNewIncomingVoIPPushPayload for \(invitation.contact.id)")
CXProvider.reportNewIncomingVoIPPushPayload([ CXProvider.reportNewIncomingVoIPPushPayload([
@@ -362,13 +480,85 @@ class NotificationService: UNNotificationServiceExtension {
"callTs": invitation.callTs.timeIntervalSince1970 "callTs": invitation.callTs.timeIntervalSince1970
]) { error in ]) { error in
logger.debug("reportNewIncomingVoIPPushPayload result: \(error)") logger.debug("reportNewIncomingVoIPPushPayload result: \(error)")
deliver(error == nil ? nil : createCallInvitationNtf(invitation)) handler(error == nil ? UNMutableNotificationContent() : createCallInvitationNtf(invitation, self.badgeCount))
} }
case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet case .empty:
case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo handler(UNMutableNotificationContent()) // used to mute notifications that did not unsubscribe yet
} }
} }
} }
private func prepareNotification() -> NSENotification? {
if expectedMessages.isEmpty {
return serviceBestAttemptNtf
} else if let callNtfKV = expectedMessages.first(where: { $0.value.msgBestAttemptNtf?.callInvitation != nil }),
let callInv = callNtfKV.value.msgBestAttemptNtf?.callInvitation,
let callNtf = callNtfKV.value.msgBestAttemptNtf {
return useCallKit() ? .callkit(callInv) : .nse(callNtf.notificationContent(badgeCount))
} else {
let ntfEvents = expectedMessages.compactMap { $0.value.msgBestAttemptNtf?.notificationEvent }
if ntfEvents.isEmpty {
return .empty
} else if let ntfEvent = ntfEvents.count == 1 ? ntfEvents.first : nil {
return .nse(ntfEvent.notificationContent(badgeCount))
} else {
return .nse(createJointNtf(ntfEvents))
}
}
}
private func createJointNtf(_ ntfEvents: [NSENotificationData]) -> UNMutableNotificationContent {
let previewMode = ntfPreviewModeGroupDefault.get()
let newMsgsData: [(any UserLike, ChatInfo)] = ntfEvents.compactMap { $0.newMsgData }
if !newMsgsData.isEmpty, let userId = newMsgsData.first?.0.userId {
let newMsgsChats: [ChatInfo] = newMsgsData.map { $0.1 }
let uniqueChatsNames = uniqueNewMsgsChatsNames(newMsgsChats)
var body: String
if previewMode == .hidden {
body = String.localizedStringWithFormat(NSLocalizedString("New messages in %d chats", comment: "notification body"), uniqueChatsNames.count)
} else {
body = String.localizedStringWithFormat(NSLocalizedString("From: %@", comment: "notification body"), newMsgsChatsNamesStr(uniqueChatsNames))
}
return createNotification(
categoryIdentifier: ntfCategoryManyEvents,
title: NSLocalizedString("New messages", comment: "notification"),
body: body,
userInfo: ["userId": userId],
badgeCount: badgeCount
)
} else {
return createNotification(
categoryIdentifier: ntfCategoryManyEvents,
title: NSLocalizedString("New events", comment: "notification"),
body: String.localizedStringWithFormat(NSLocalizedString("%d new events", comment: "notification body"), ntfEvents.count),
badgeCount: badgeCount
)
}
}
private func uniqueNewMsgsChatsNames(_ newMsgsChats: [ChatInfo]) -> [String] {
var seenChatIds = Set<ChatId>()
var uniqueChatsNames: [String] = []
for chat in newMsgsChats {
if !seenChatIds.contains(chat.id) {
seenChatIds.insert(chat.id)
uniqueChatsNames.append(chat.chatViewName)
}
}
return uniqueChatsNames
}
private func newMsgsChatsNamesStr(_ names: [String]) -> String {
return switch names.count {
case 1: names[0]
case 2: "\(names[0]) and \(names[1])"
case 3: "\(names[0] + ", " + names[1]) and \(names[2])"
default:
names.count > 3
? "\(names[0]), \(names[1]) and \(names.count - 2) other chats"
: ""
}
}
} }
// nseStateGroupDefault must not be used in NSE directly, only via this singleton // nseStateGroupDefault must not be used in NSE directly, only via this singleton
@@ -582,28 +772,25 @@ func chatRecvMsg() async -> ChatResponse? {
private let isInChina = SKStorefront().countryCode == "CHN" private let isInChina = SKStorefront().countryCode == "CHN"
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() } private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotificationData)? {
logger.debug("NotificationService receivedMsgNtf: \(res.responseType)") logger.debug("NotificationService receivedMsgNtf: \(res.responseType)")
switch res { switch res {
case let .contactConnected(user, contact, _): case let .contactConnected(user, contact, _):
return (contact.id, .nse(createContactConnectedNtf(user, contact))) return (contact.id, .contactConnected(user, contact))
// case let .contactConnecting(contact): // case let .contactConnecting(contact):
// TODO profile update // TODO profile update
case let .receivedContactRequest(user, contactRequest): case let .receivedContactRequest(user, contactRequest):
return (UserContact(contactRequest: contactRequest).id, .nse(createContactRequestNtf(user, contactRequest))) return (UserContact(contactRequest: contactRequest).id, .contactRequest(user, contactRequest))
case let .newChatItems(user, chatItems): case let .newChatItems(user, chatItems):
// Received items are created one at a time // Received items are created one at a time
if let chatItem = chatItems.first { if let chatItem = chatItems.first {
let cInfo = chatItem.chatInfo let cInfo = chatItem.chatInfo
var cItem = chatItem.chatItem var cItem = chatItem.chatItem
if !cInfo.ntfsEnabled {
ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1))
}
if let file = cItem.autoReceiveFile() { if let file = cItem.autoReceiveFile() {
cItem = autoReceiveFile(file) ?? cItem cItem = autoReceiveFile(file) ?? cItem
} }
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(createMessageReceivedNtf(user, cInfo, cItem)) : .empty let ntf: NSENotificationData = (cInfo.ntfsEnabled && cItem.showNotification) ? .messageReceived(user, cInfo, cItem) : .noNtf
return cItem.showNotification ? (chatItem.chatId, ntf) : nil return (chatItem.chatId, ntf)
} else { } else {
return nil return nil
} }
@@ -620,10 +807,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
return nil return nil
case let .callInvitation(invitation): case let .callInvitation(invitation):
// Do not post it without CallKit support, iOS will stop launching the app without showing CallKit // Do not post it without CallKit support, iOS will stop launching the app without showing CallKit
return ( return (invitation.contact.id, .callInvitation(invitation))
invitation.contact.id,
useCallKit() ? .callkit(invitation) : .nse(createCallInvitationNtf(invitation))
)
case let .ntfMessage(_, connEntity, ntfMessage): case let .ntfMessage(_, connEntity, ntfMessage):
return if let id = connEntity.id { (id, .msgInfo(ntfMessage)) } else { nil } return if let id = connEntity.id { (id, .msgInfo(ntfMessage)) } else { nil }
case .chatSuspended: case .chatSuspended:
@@ -704,15 +888,15 @@ func apiSetEncryptLocalFiles(_ enable: Bool) throws {
throw r throw r
} }
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { func apiGetNtfConns(nonce: String, encNtfInfo: String) -> [UserNtfConn]? {
guard apiGetActiveUser() != nil else { guard apiGetActiveUser() != nil else {
logger.debug("no active user") logger.debug("no active user")
return nil return nil
} }
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)) let r = sendSimpleXCmd(.apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo))
if case let .ntfMessages(user, connEntity_, expectedMsg_, receivedMsg_) = r, let user = user { if case let .ntfConns(ntfConns) = r {
logger.debug("apiGetNtfMessage response ntfMessages: \(receivedMsg_ == nil ? 0 : 1)") logger.debug("apiGetNtfConns response ntfConns: \(ntfConns.count)")
return NtfMessages(user: user, connEntity_: connEntity_, expectedMsg_: expectedMsg_, receivedMsg_: receivedMsg_) return ntfConns.compactMap { toUserNtfConn($0) }
} else if case let .chatCmdError(_, error) = r { } else if case let .chatCmdError(_, error) = r {
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))") logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
} else { } else {
@@ -721,17 +905,33 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
return nil return nil
} }
func apiGetConnNtfMessage(connId: String) -> NtfMsgInfo? { func toUserNtfConn(_ ntfConn: NtfConn) -> UserNtfConn? {
if let user = ntfConn.user_ {
return UserNtfConn(user: user, connEntity_: ntfConn.connEntity_, expectedMsg_: ntfConn.expectedMsg_)
} else {
return nil
}
}
func apiGetConnNtfMessages(connIds: [String]) -> [NtfMsgInfo?]? {
guard apiGetActiveUser() != nil else { guard apiGetActiveUser() != nil else {
logger.debug("no active user") logger.debug("no active user")
return nil return nil
} }
let r = sendSimpleXCmd(.apiGetConnNtfMessage(connId: connId)) let r = sendSimpleXCmd(.apiGetConnNtfMessages(connIds: connIds))
if case let .connNtfMessage(receivedMsg_) = r { if case let .connNtfMessages(receivedMsgs) = r {
logger.debug("apiGetConnNtfMessage response receivedMsg_: \(receivedMsg_ == nil ? 0 : 1)") logger.debug("apiGetConnNtfMessages response receivedMsgs: \(receivedMsgs.count)")
return receivedMsg_ return receivedMsgs
}
logger.debug("apiGetConnNtfMessages error: \(responseError(r))")
return nil
}
func getConnNtfMessage(connId: String) -> NtfMsgInfo? {
let r_ = apiGetConnNtfMessages(connIds: [connId])
if let r = r_, let receivedMsg = r.count == 1 ? r.first : nil {
return receivedMsg
} }
logger.debug("apiGetConnNtfMessage error: \(responseError(r))")
return nil return nil
} }
@@ -769,13 +969,33 @@ func setNetworkConfig(_ cfg: NetCfg) throws {
throw r throw r
} }
struct NtfMessages { struct UserNtfConn {
var user: User var user: User
var connEntity_: ConnectionEntity? var connEntity_: ConnectionEntity?
var expectedMsg_: NtfMsgInfo? var expectedMsg_: NtfMsgInfo?
var receivedMsg_: NtfMsgInfo?
var ntfsEnabled: Bool { var defaultBestAttemptNtf: NSENotificationData {
user.showNotifications && (connEntity_?.ntfsEnabled ?? false) return if !user.showNotifications {
.noNtf
} else if let connEntity = connEntity_ {
switch connEntity {
case let .rcvDirectMsgConnection(_, contact):
contact?.chatSettings.enableNtfs == .all
? .connectionEvent(user, connEntity)
: .noNtf
case let .rcvGroupMsgConnection(_, groupInfo, _):
groupInfo.chatSettings.enableNtfs == .all
? .connectionEvent(user, connEntity)
: .noNtf
case .sndFileConnection: .noNtf
case .rcvFileConnection: .noNtf
case let .userContactConnection(_, userContact):
userContact.groupId == nil
? .connectionEvent(user, connEntity)
: .noNtf
}
} else {
.noNtf
}
} }
} }
+57 -45
View File
@@ -144,15 +144,15 @@
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */; }; 640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */; };
640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CC2B29B8C200CCB412 /* NewChatView.swift */; }; 640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CC2B29B8C200CCB412 /* NewChatView.swift */; };
640548A32CB56735005DE1E4 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6405489E2CB56735005DE1E4 /* libgmpxx.a */; };
640548A42CB56735005DE1E4 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6405489F2CB56735005DE1E4 /* libgmp.a */; };
640548A52CB56735005DE1E4 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 640548A02CB56735005DE1E4 /* libffi.a */; };
640548A62CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 640548A12CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv-ghc9.6.3.a */; };
640548A72CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 640548A22CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a */; };
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; }; 6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; }; 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; };
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; };
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B402CCBEB080083A2CF /* libgmpxx.a */; };
643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */; };
643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B422CCBEB080083A2CF /* libffi.a */; };
643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */; };
643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 643B3B442CCBEB080083A2CF /* libgmp.a */; };
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; }; 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
@@ -209,6 +209,7 @@
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = CE38A29B2C3FCD72005ED185 /* SwiftyGif */; }; CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = CE38A29B2C3FCD72005ED185 /* SwiftyGif */; };
CE75480A2C622630009579B7 /* SwipeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7548092C622630009579B7 /* SwipeLabel.swift */; }; CE75480A2C622630009579B7 /* SwipeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7548092C622630009579B7 /* SwipeLabel.swift */; };
CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */; }; CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */; };
CEA6E91C2CBD21B0002B5DB4 /* UserDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA6E91B2CBD21B0002B5DB4 /* UserDefault.swift */; };
CEDB245B2C9CD71800FBC5F6 /* StickyScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */; }; CEDB245B2C9CD71800FBC5F6 /* StickyScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */; };
CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDE70212C48FD9500233B1F /* SEChatState.swift */; }; CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDE70212C48FD9500233B1F /* SEChatState.swift */; };
CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */; }; CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */; };
@@ -486,15 +487,15 @@
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatMenuButton.swift; sourceTree = "<group>"; }; 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatMenuButton.swift; sourceTree = "<group>"; };
640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = "<group>"; }; 640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = "<group>"; };
6405489E2CB56735005DE1E4 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
6405489F2CB56735005DE1E4 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
640548A02CB56735005DE1E4 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
640548A12CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv-ghc9.6.3.a"; sourceTree = "<group>"; };
640548A22CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a"; sourceTree = "<group>"; };
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; }; 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; }; 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; };
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
643B3B402CCBEB080083A2CF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = "<group>"; };
643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a"; sourceTree = "<group>"; };
643B3B422CCBEB080083A2CF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = "<group>"; };
643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a"; path = "Libraries/libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a"; sourceTree = "<group>"; };
643B3B442CCBEB080083A2CF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = Libraries/libgmp.a; sourceTree = "<group>"; };
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; }; 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; }; 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; };
@@ -550,6 +551,7 @@
CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; }; CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; };
CE7548092C622630009579B7 /* SwipeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeLabel.swift; sourceTree = "<group>"; }; CE7548092C622630009579B7 /* SwipeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeLabel.swift; sourceTree = "<group>"; };
CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.swift; sourceTree = "<group>"; }; CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.swift; sourceTree = "<group>"; };
CEA6E91B2CBD21B0002B5DB4 /* UserDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = "<group>"; };
CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickyScrollView.swift; sourceTree = "<group>"; }; CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickyScrollView.swift; sourceTree = "<group>"; };
CEDE70212C48FD9500233B1F /* SEChatState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEChatState.swift; sourceTree = "<group>"; }; CEDE70212C48FD9500233B1F /* SEChatState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEChatState.swift; sourceTree = "<group>"; };
CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX SE.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX SE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -655,14 +657,14 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
640548A32CB56735005DE1E4 /* libgmpxx.a in Frameworks */, 643B3B452CCBEB080083A2CF /* libgmpxx.a in Frameworks */,
640548A72CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a in Frameworks */, 643B3B472CCBEB080083A2CF /* libffi.a in Frameworks */,
640548A52CB56735005DE1E4 /* libffi.a in Frameworks */, 643B3B492CCBEB080083A2CF /* libgmp.a in Frameworks */,
640548A42CB56735005DE1E4 /* libgmp.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
640548A62CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv-ghc9.6.3.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
643B3B482CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a in Frameworks */,
643B3B462CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -739,11 +741,6 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
640548A02CB56735005DE1E4 /* libffi.a */,
6405489F2CB56735005DE1E4 /* libgmp.a */,
6405489E2CB56735005DE1E4 /* libgmpxx.a */,
640548A12CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv-ghc9.6.3.a */,
640548A22CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -802,6 +799,7 @@
CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */, CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */,
CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */, CEDB245A2C9CD71800FBC5F6 /* StickyScrollView.swift */,
CEFB2EDE2CA1BCC7004B1ECE /* SheetRepresentable.swift */, CEFB2EDE2CA1BCC7004B1ECE /* SheetRepresentable.swift */,
CEA6E91B2CBD21B0002B5DB4 /* UserDefault.swift */,
); );
path = Helpers; path = Helpers;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -814,6 +812,11 @@
5CC2C0FA2809BF11000C35E3 /* Localizable.strings */, 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */,
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */, 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */,
5C764E5C279C70B7000C6508 /* Libraries */, 5C764E5C279C70B7000C6508 /* Libraries */,
643B3B422CCBEB080083A2CF /* libffi.a */,
643B3B442CCBEB080083A2CF /* libgmp.a */,
643B3B402CCBEB080083A2CF /* libgmpxx.a */,
643B3B412CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5-ghc9.6.3.a */,
643B3B432CCBEB080083A2CF /* libHSsimplex-chat-6.1.1.0-CTfGB7l09cqEHVIdvhrnH5.a */,
5CA059C2279559F40002BEB4 /* Shared */, 5CA059C2279559F40002BEB4 /* Shared */,
5CDCAD462818589900503DA2 /* SimpleX NSE */, 5CDCAD462818589900503DA2 /* SimpleX NSE */,
CEE723A82C3BD3D70009AE93 /* SimpleX SE */, CEE723A82C3BD3D70009AE93 /* SimpleX SE */,
@@ -909,10 +912,9 @@
5CB924DF27A8678B00ACCCDD /* UserSettings */ = { 5CB924DF27A8678B00ACCCDD /* UserSettings */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */,
5CB924D627A8563F00ACCCDD /* SettingsView.swift */, 5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
5CB346E62868D76D001FD2EF /* NotificationsView.swift */, 5CB346E62868D76D001FD2EF /* NotificationsView.swift */,
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */,
5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */,
5CADE79929211BB900072E13 /* PreferencesView.swift */, 5CADE79929211BB900072E13 /* PreferencesView.swift */,
5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */, 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */,
5C05DF522840AA1D00C683F9 /* CallSettings.swift */, 5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
@@ -920,9 +922,6 @@
5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */, 5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */,
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */, 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */, 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */,
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */,
5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */,
5CB2084E28DA4B4800D024EC /* RTCServers.swift */, 5CB2084E28DA4B4800D024EC /* RTCServers.swift */,
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */, 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */,
18415845648CA4F5A8BCA272 /* UserProfilesView.swift */, 18415845648CA4F5A8BCA272 /* UserProfilesView.swift */,
@@ -1053,6 +1052,18 @@
path = Database; path = Database;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */ = {
isa = PBXGroup;
children = (
5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */,
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */,
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */,
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */,
5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */,
);
path = NetworkAndServers;
sourceTree = "<group>";
};
6440CA01288AEC770062C672 /* Group */ = { 6440CA01288AEC770062C672 /* Group */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1482,6 +1493,7 @@
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */, 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */, 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */, 5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */,
CEA6E91C2CBD21B0002B5DB4 /* UserDefault.swift in Sources */,
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */, 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */,
5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */, 5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */,
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */, 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */,
@@ -1899,7 +1911,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 242; CURRENT_PROJECT_VERSION = 245;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -1924,7 +1936,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
LLVM_LTO = YES_THIN; LLVM_LTO = YES_THIN;
MARKETING_VERSION = 6.1; MARKETING_VERSION = 6.1.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1948,7 +1960,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 242; CURRENT_PROJECT_VERSION = 245;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -1973,7 +1985,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 6.1; MARKETING_VERSION = 6.1.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1989,11 +2001,11 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 242; CURRENT_PROJECT_VERSION = 245;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.1; MARKETING_VERSION = 6.1.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -2009,11 +2021,11 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 242; CURRENT_PROJECT_VERSION = 245;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.1; MARKETING_VERSION = 6.1.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -2034,7 +2046,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 242; CURRENT_PROJECT_VERSION = 245;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s; GCC_OPTIMIZATION_LEVEL = s;
@@ -2049,7 +2061,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 6.1; MARKETING_VERSION = 6.1.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2071,7 +2083,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 242; CURRENT_PROJECT_VERSION = 245;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO; ENABLE_CODE_COVERAGE = NO;
@@ -2086,7 +2098,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 6.1; MARKETING_VERSION = 6.1.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2108,7 +2120,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 242; CURRENT_PROJECT_VERSION = 245;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2134,7 +2146,7 @@
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 6.1; MARKETING_VERSION = 6.1.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -2159,7 +2171,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 242; CURRENT_PROJECT_VERSION = 245;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2185,7 +2197,7 @@
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 6.1; MARKETING_VERSION = 6.1.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -2210,7 +2222,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 242; CURRENT_PROJECT_VERSION = 245;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2225,7 +2237,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.1; MARKETING_VERSION = 6.1.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -2244,7 +2256,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 242; CURRENT_PROJECT_VERSION = 245;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2259,7 +2271,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
LOCALIZATION_PREFERS_STRING_CATALOGS = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.1; MARKETING_VERSION = 6.1.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
+14 -13
View File
@@ -55,8 +55,8 @@ public enum ChatCommand {
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
case apiVerifyToken(token: DeviceToken, nonce: String, code: String) case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
case apiDeleteToken(token: DeviceToken) case apiDeleteToken(token: DeviceToken)
case apiGetNtfMessage(nonce: String, encNtfInfo: String) case apiGetNtfConns(nonce: String, encNtfInfo: String)
case apiGetConnNtfMessage(connId: String) case apiGetConnNtfMessages(connIds: [String])
case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile) case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile)
case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole)
case apiJoinGroup(groupId: Int64) case apiJoinGroup(groupId: Int64)
@@ -214,8 +214,8 @@ public enum ChatCommand {
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)"
case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)" case let .apiGetNtfConns(nonce, encNtfInfo): return "/_ntf conns \(nonce) \(encNtfInfo)"
case let .apiGetConnNtfMessage(connId): return "/_ntf conn message \(connId)" case let .apiGetConnNtfMessages(connIds): return "/_ntf conn messages \(connIds.joined(separator: ","))"
case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))" case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))"
case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)" case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)"
case let .apiJoinGroup(groupId): return "/_join #\(groupId)" case let .apiJoinGroup(groupId): return "/_join #\(groupId)"
@@ -369,8 +369,8 @@ public enum ChatCommand {
case .apiRegisterToken: return "apiRegisterToken" case .apiRegisterToken: return "apiRegisterToken"
case .apiVerifyToken: return "apiVerifyToken" case .apiVerifyToken: return "apiVerifyToken"
case .apiDeleteToken: return "apiDeleteToken" case .apiDeleteToken: return "apiDeleteToken"
case .apiGetNtfMessage: return "apiGetNtfMessage" case .apiGetNtfConns: return "apiGetNtfConns"
case .apiGetConnNtfMessage: return "apiGetConnNtfMessage" case .apiGetConnNtfMessages: return "apiGetConnNtfMessages"
case .apiNewGroup: return "apiNewGroup" case .apiNewGroup: return "apiNewGroup"
case .apiAddMember: return "apiAddMember" case .apiAddMember: return "apiAddMember"
case .apiJoinGroup: return "apiJoinGroup" case .apiJoinGroup: return "apiJoinGroup"
@@ -682,8 +682,8 @@ public enum ChatResponse: Decodable, Error {
case callInvitations(callInvitations: [RcvCallInvitation]) case callInvitations(callInvitations: [RcvCallInvitation])
case ntfTokenStatus(status: NtfTknStatus) case ntfTokenStatus(status: NtfTknStatus)
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String) case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String)
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, expectedMsg_: NtfMsgInfo?, receivedMsg_: NtfMsgInfo?) case ntfConns(ntfConns: [NtfConn])
case connNtfMessage(receivedMsg_: NtfMsgInfo?) case connNtfMessages(receivedMsgs: [NtfMsgInfo?])
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo) case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo)
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
case contactDisabled(user: UserRef, contact: Contact) case contactDisabled(user: UserRef, contact: Contact)
@@ -851,8 +851,8 @@ public enum ChatResponse: Decodable, Error {
case .callInvitations: return "callInvitations" case .callInvitations: return "callInvitations"
case .ntfTokenStatus: return "ntfTokenStatus" case .ntfTokenStatus: return "ntfTokenStatus"
case .ntfToken: return "ntfToken" case .ntfToken: return "ntfToken"
case .ntfMessages: return "ntfMessages" case .ntfConns: return "ntfConns"
case .connNtfMessage: return "connNtfMessage" case .connNtfMessages: return "connNtfMessages"
case .ntfMessage: return "ntfMessage" case .ntfMessage: return "ntfMessage"
case .contactConnectionDeleted: return "contactConnectionDeleted" case .contactConnectionDeleted: return "contactConnectionDeleted"
case .contactDisabled: return "contactDisabled" case .contactDisabled: return "contactDisabled"
@@ -1029,8 +1029,8 @@ public enum ChatResponse: Decodable, Error {
case let .callInvitations(invs): return String(describing: invs) case let .callInvitations(invs): return String(describing: invs)
case let .ntfTokenStatus(status): return String(describing: status) case let .ntfTokenStatus(status): return String(describing: status)
case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)" case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)"
case let .ntfMessages(u, connEntity, expectedMsg_, receivedMsg_): return withUser(u, "connEntity: \(String(describing: connEntity))\nexpectedMsg_: \(String(describing: expectedMsg_))\nreceivedMsg_: \(String(describing: receivedMsg_))") case let .ntfConns(ntfConns): return String(describing: ntfConns)
case let .connNtfMessage(receivedMsg_): return "receivedMsg_: \(String(describing: receivedMsg_))" case let .connNtfMessages(receivedMsgs): return "receivedMsgs: \(String(describing: receivedMsgs))"
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))") case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection)) case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
case let .contactDisabled(u, contact): return withUser(u, String(describing: contact)) case let .contactDisabled(u, contact): return withUser(u, String(describing: contact))
@@ -2041,7 +2041,7 @@ public enum SQLiteError: Decodable, Hashable {
} }
public enum AgentErrorType: Decodable, Hashable { public enum AgentErrorType: Decodable, Hashable {
case CMD(cmdErr: CommandErrorType) case CMD(cmdErr: CommandErrorType, errContext: String)
case CONN(connErr: ConnectionErrorType) case CONN(connErr: ConnectionErrorType)
case SMP(serverAddress: String, smpErr: ProtocolErrorType) case SMP(serverAddress: String, smpErr: ProtocolErrorType)
case NTF(ntfErr: ProtocolErrorType) case NTF(ntfErr: ProtocolErrorType)
@@ -2088,6 +2088,7 @@ public enum ProtocolErrorType: Decodable, Hashable {
case AUTH case AUTH
case CRYPTO case CRYPTO
case QUOTA case QUOTA
case STORE(storeErr: String)
case NO_MSG case NO_MSG
case LARGE_MSG case LARGE_MSG
case EXPIRED case EXPIRED
+1 -1
View File
@@ -85,7 +85,7 @@ public func registerGroupDefaults() {
GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false, GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false,
GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED: true, GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED: true,
GROUP_DEFAULT_ALLOW_SHARE_EXTENSION: false, GROUP_DEFAULT_ALLOW_SHARE_EXTENSION: false,
GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS: false, GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS: true,
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true, GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false, GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false,
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true, GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
+8 -9
View File
@@ -1852,6 +1852,7 @@ public struct PendingContactConnection: Decodable, NamedChat, Hashable {
public enum ConnStatus: String, Decodable, Hashable { public enum ConnStatus: String, Decodable, Hashable {
case new = "new" case new = "new"
case prepared = "prepared"
case joined = "joined" case joined = "joined"
case requested = "requested" case requested = "requested"
case accepted = "accepted" case accepted = "accepted"
@@ -1863,6 +1864,7 @@ public enum ConnStatus: String, Decodable, Hashable {
get { get {
switch self { switch self {
case .new: return true case .new: return true
case .prepared: return false
case .joined: return false case .joined: return false
case .requested: return true case .requested: return true
case .accepted: return true case .accepted: return true
@@ -2268,16 +2270,13 @@ public enum ConnectionEntity: Decodable, Hashable {
case let .userContactConnection(entityConnection, _): entityConnection case let .userContactConnection(entityConnection, _): entityConnection
} }
} }
}
public struct NtfConn: Decodable, Hashable {
public var user_: User?
public var connEntity_: ConnectionEntity?
public var expectedMsg_: NtfMsgInfo?
public var ntfsEnabled: Bool {
switch self {
case let .rcvDirectMsgConnection(_, contact): return contact?.chatSettings.enableNtfs == .all
case let .rcvGroupMsgConnection(_, groupInfo, _): return groupInfo.chatSettings.enableNtfs == .all
case .sndFileConnection: return false
case .rcvFileConnection: return false
case let .userContactConnection(_, userContact): return userContact.groupId == nil
}
}
} }
public struct NtfMsgInfo: Decodable, Hashable { public struct NtfMsgInfo: Decodable, Hashable {
+32 -16
View File
@@ -15,13 +15,14 @@ public let ntfCategoryContactConnected = "NTF_CAT_CONTACT_CONNECTED"
public let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED" public let ntfCategoryMessageReceived = "NTF_CAT_MESSAGE_RECEIVED"
public let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION" public let ntfCategoryCallInvitation = "NTF_CAT_CALL_INVITATION"
public let ntfCategoryConnectionEvent = "NTF_CAT_CONNECTION_EVENT" public let ntfCategoryConnectionEvent = "NTF_CAT_CONNECTION_EVENT"
public let ntfCategoryManyEvents = "NTF_CAT_MANY_EVENTS"
public let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE" public let ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE"
public let appNotificationId = "chat.simplex.app.notification" public let appNotificationId = "chat.simplex.app.notification"
let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification") let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification")
public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: UserContactRequest) -> UNMutableNotificationContent { public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: UserContactRequest, _ badgeCount: Int) -> UNMutableNotificationContent {
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
return createNotification( return createNotification(
categoryIdentifier: ntfCategoryContactRequest, categoryIdentifier: ntfCategoryContactRequest,
@@ -34,11 +35,12 @@ public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: User
hideContent ? NSLocalizedString("this contact", comment: "notification title") : contactRequest.chatViewName hideContent ? NSLocalizedString("this contact", comment: "notification title") : contactRequest.chatViewName
), ),
targetContentIdentifier: nil, targetContentIdentifier: nil,
userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId, "userId": user.userId] userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId, "userId": user.userId],
badgeCount: badgeCount
) )
} }
public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact) -> UNMutableNotificationContent { public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact, _ badgeCount: Int) -> UNMutableNotificationContent {
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
return createNotification( return createNotification(
categoryIdentifier: ntfCategoryContactConnected, categoryIdentifier: ntfCategoryContactConnected,
@@ -51,12 +53,13 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact)
hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName
), ),
targetContentIdentifier: contact.id, targetContentIdentifier: contact.id,
userInfo: ["userId": user.userId] userInfo: ["userId": user.userId],
// userInfo: ["chatId": contact.id, "contactId": contact.apiId] // userInfo: ["chatId": contact.id, "contactId": contact.apiId]
badgeCount: badgeCount
) )
} }
public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent { public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem, _ badgeCount: Int) -> UNMutableNotificationContent {
let previewMode = ntfPreviewModeGroupDefault.get() let previewMode = ntfPreviewModeGroupDefault.get()
var title: String var title: String
if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir { if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir {
@@ -69,12 +72,13 @@ public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _
title: title, title: title,
body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"), body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"),
targetContentIdentifier: cInfo.id, targetContentIdentifier: cInfo.id,
userInfo: ["userId": user.userId] userInfo: ["userId": user.userId],
// userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id] // userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id]
badgeCount: badgeCount
) )
} }
public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutableNotificationContent { public func createCallInvitationNtf(_ invitation: RcvCallInvitation, _ badgeCount: Int) -> UNMutableNotificationContent {
let text = invitation.callType.media == .video let text = invitation.callType.media == .video
? NSLocalizedString("Incoming video call", comment: "notification") ? NSLocalizedString("Incoming video call", comment: "notification")
: NSLocalizedString("Incoming audio call", comment: "notification") : NSLocalizedString("Incoming audio call", comment: "notification")
@@ -84,11 +88,12 @@ public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutabl
title: hideContent ? contactHidden : "\(invitation.contact.chatViewName):", title: hideContent ? contactHidden : "\(invitation.contact.chatViewName):",
body: text, body: text,
targetContentIdentifier: nil, targetContentIdentifier: nil,
userInfo: ["chatId": invitation.contact.id, "userId": invitation.user.userId] userInfo: ["chatId": invitation.contact.id, "userId": invitation.user.userId],
badgeCount: badgeCount
) )
} }
public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity) -> UNMutableNotificationContent { public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity, _ badgeCount: Int) -> UNMutableNotificationContent {
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
var title: String var title: String
var body: String? = nil var body: String? = nil
@@ -118,11 +123,12 @@ public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntit
title: title, title: title,
body: body, body: body,
targetContentIdentifier: targetContentIdentifier, targetContentIdentifier: targetContentIdentifier,
userInfo: ["userId": user.userId] userInfo: ["userId": user.userId],
badgeCount: badgeCount
) )
} }
public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificationContent { public func createErrorNtf(_ dbStatus: DBMigrationResult, _ badgeCount: Int) -> UNMutableNotificationContent {
var title: String var title: String
switch dbStatus { switch dbStatus {
case .errorNotADatabase: case .errorNotADatabase:
@@ -142,14 +148,16 @@ public func createErrorNtf(_ dbStatus: DBMigrationResult) -> UNMutableNotificati
} }
return createNotification( return createNotification(
categoryIdentifier: ntfCategoryConnectionEvent, categoryIdentifier: ntfCategoryConnectionEvent,
title: title title: title,
badgeCount: badgeCount
) )
} }
public func createAppStoppedNtf() -> UNMutableNotificationContent { public func createAppStoppedNtf(_ badgeCount: Int) -> UNMutableNotificationContent {
return createNotification( return createNotification(
categoryIdentifier: ntfCategoryConnectionEvent, categoryIdentifier: ntfCategoryConnectionEvent,
title: NSLocalizedString("Encrypted message: app is stopped", comment: "notification") title: NSLocalizedString("Encrypted message: app is stopped", comment: "notification"),
badgeCount: badgeCount
) )
} }
@@ -159,8 +167,15 @@ private func groupMsgNtfTitle(_ groupInfo: GroupInfo, _ groupMember: GroupMember
: "#\(groupInfo.displayName) \(groupMember.chatViewName):" : "#\(groupInfo.displayName) \(groupMember.chatViewName):"
} }
public func createNotification(categoryIdentifier: String, title: String, subtitle: String? = nil, body: String? = nil, public func createNotification(
targetContentIdentifier: String? = nil, userInfo: [AnyHashable : Any] = [:]) -> UNMutableNotificationContent { categoryIdentifier: String,
title: String,
subtitle: String? = nil,
body: String? = nil,
targetContentIdentifier: String? = nil,
userInfo: [AnyHashable : Any] = [:],
badgeCount: Int
) -> UNMutableNotificationContent {
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.categoryIdentifier = categoryIdentifier content.categoryIdentifier = categoryIdentifier
content.title = title content.title = title
@@ -170,6 +185,7 @@ public func createNotification(categoryIdentifier: String, title: String, subtit
content.userInfo = userInfo content.userInfo = userInfo
// TODO move logic of adding sound here, so it applies to background notifications too // TODO move logic of adding sound here, so it applies to background notifications too
content.sound = .default content.sound = .default
content.badge = badgeCount as NSNumber
// content.interruptionLevel = .active // content.interruptionLevel = .active
// content.relevanceScore = 0.5 // 0-1 // content.relevanceScore = 0.5 // 0-1
return content return content
+36
View File
@@ -694,15 +694,30 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Bad message ID" = "Falsche Nachrichten-ID"; "Bad message ID" = "Falsche Nachrichten-ID";
/* No comment provided by engineer. */
"Better calls" = "Verbesserte Anrufe";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better groups" = "Bessere Gruppen"; "Better groups" = "Bessere Gruppen";
/* No comment provided by engineer. */
"Better message dates." = "Verbesserte Nachrichten-Datumsinformation";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better messages" = "Verbesserungen bei Nachrichten"; "Better messages" = "Verbesserungen bei Nachrichten";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better networking" = "Kontrollieren Sie Ihr Netzwerk"; "Better networking" = "Kontrollieren Sie Ihr Netzwerk";
/* No comment provided by engineer. */
"Better notifications" = "Verbesserte Benachrichtigungen";
/* No comment provided by engineer. */
"Better security ✅" = "Verbesserte Sicherheit ✅";
/* No comment provided by engineer. */
"Better user experience" = "Verbesserte Nutzer-Erfahrung";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Black" = "Schwarz"; "Black" = "Schwarz";
@@ -1292,6 +1307,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Custom time" = "Zeit anpassen"; "Custom time" = "Zeit anpassen";
/* No comment provided by engineer. */
"Customizable message shape." = "Anpassbares Format des Nachrichtenfelds";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Customize theme" = "Design anpassen"; "Customize theme" = "Design anpassen";
@@ -1482,6 +1500,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete old database?" = "Alte Datenbank löschen?"; "Delete old database?" = "Alte Datenbank löschen?";
/* No comment provided by engineer. */
"Delete or moderate up to 200 messages." = "Bis zu 200 Nachrichten löschen oder moderieren";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete pending connection?" = "Ausstehende Verbindung löschen?"; "Delete pending connection?" = "Ausstehende Verbindung löschen?";
@@ -2230,6 +2251,9 @@
/* alert message */ /* alert message */
"Forward messages without files?" = "Nachrichten ohne Dateien weiterleiten?"; "Forward messages without files?" = "Nachrichten ohne Dateien weiterleiten?";
/* No comment provided by engineer. */
"Forward up to 20 messages at once." = "Bis zu 20 Nachrichten auf einmal weiterleiten";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"forwarded" = "weitergeleitet"; "forwarded" = "weitergeleitet";
@@ -2470,6 +2494,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Importing archive" = "Archiv wird importiert"; "Importing archive" = "Archiv wird importiert";
/* No comment provided by engineer. */
"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Verbesserte Nachrichten-Auslieferung und verringerter Datenverbrauch.\nWeitere Verbesserungen sind bald verfügbar!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Improved message delivery" = "Verbesserte Zustellung von Nachrichten"; "Improved message delivery" = "Verbesserte Zustellung von Nachrichten";
@@ -4271,6 +4298,9 @@
/* simplex link type */ /* simplex link type */
"SimpleX one-time invitation" = "SimpleX-Einmal-Einladung"; "SimpleX one-time invitation" = "SimpleX-Einmal-Einladung";
/* No comment provided by engineer. */
"SimpleX protocols reviewed by Trail of Bits." = "Die SimpleX-Protokolle wurden von Trail of Bits überprüft.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Simplified incognito mode" = "Vereinfachter Inkognito-Modus"; "Simplified incognito mode" = "Vereinfachter Inkognito-Modus";
@@ -4391,6 +4421,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Support SimpleX Chat" = "Unterstützung von SimpleX Chat"; "Support SimpleX Chat" = "Unterstützung von SimpleX Chat";
/* No comment provided by engineer. */
"Switch audio and video during the call." = "Während des Anrufs zwischen Audio und Video wechseln";
/* No comment provided by engineer. */
"Switch chat profile for 1-time invitations." = "Das Chat-Profil für Einmal-Einladungen wechseln";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"System" = "System"; "System" = "System";
+36
View File
@@ -694,15 +694,30 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Bad message ID" = "ID de mensaje incorrecto"; "Bad message ID" = "ID de mensaje incorrecto";
/* No comment provided by engineer. */
"Better calls" = "Llamadas mejoradas";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better groups" = "Grupos mejorados"; "Better groups" = "Grupos mejorados";
/* No comment provided by engineer. */
"Better message dates." = "Sistema de fechas mejorado.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better messages" = "Mensajes mejorados"; "Better messages" = "Mensajes mejorados";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better networking" = "Uso de red mejorado"; "Better networking" = "Uso de red mejorado";
/* No comment provided by engineer. */
"Better notifications" = "Notificaciones mejoradas";
/* No comment provided by engineer. */
"Better security ✅" = "Seguridad mejorada ✅";
/* No comment provided by engineer. */
"Better user experience" = "Experiencia de usuario mejorada";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Black" = "Negro"; "Black" = "Negro";
@@ -1292,6 +1307,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Custom time" = "Tiempo personalizado"; "Custom time" = "Tiempo personalizado";
/* No comment provided by engineer. */
"Customizable message shape." = "Forma personalizable de los mensajes.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Customize theme" = "Personalizar tema"; "Customize theme" = "Personalizar tema";
@@ -1482,6 +1500,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete old database?" = "¿Eliminar base de datos antigua?"; "Delete old database?" = "¿Eliminar base de datos antigua?";
/* No comment provided by engineer. */
"Delete or moderate up to 200 messages." = "Borra o modera hasta 200 mensajes a la vez.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete pending connection?" = "¿Eliminar conexión pendiente?"; "Delete pending connection?" = "¿Eliminar conexión pendiente?";
@@ -2230,6 +2251,9 @@
/* alert message */ /* alert message */
"Forward messages without files?" = "¿Reenviar mensajes sin los archivos?"; "Forward messages without files?" = "¿Reenviar mensajes sin los archivos?";
/* No comment provided by engineer. */
"Forward up to 20 messages at once." = "Desplazamiento de hasta 20 mensajes.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"forwarded" = "reenviado"; "forwarded" = "reenviado";
@@ -2470,6 +2494,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Importing archive" = "Importando archivo"; "Importing archive" = "Importando archivo";
/* No comment provided by engineer. */
"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Reducción del tráfico y entrega mejorada.\n¡Pronto habrá nuevas mejoras!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Improved message delivery" = "Entrega de mensajes mejorada"; "Improved message delivery" = "Entrega de mensajes mejorada";
@@ -4271,6 +4298,9 @@
/* simplex link type */ /* simplex link type */
"SimpleX one-time invitation" = "Invitación SimpleX de un uso"; "SimpleX one-time invitation" = "Invitación SimpleX de un uso";
/* No comment provided by engineer. */
"SimpleX protocols reviewed by Trail of Bits." = "Protocolos de SimpleX auditados por Trail of Bits.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Simplified incognito mode" = "Modo incógnito simplificado"; "Simplified incognito mode" = "Modo incógnito simplificado";
@@ -4391,6 +4421,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Support SimpleX Chat" = "Soporte SimpleX Chat"; "Support SimpleX Chat" = "Soporte SimpleX Chat";
/* No comment provided by engineer. */
"Switch audio and video during the call." = "Intercambia audio y video durante la llamada.";
/* No comment provided by engineer. */
"Switch chat profile for 1-time invitations." = "Cambia el perfil de chat para invitaciones de un solo uso.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"System" = "Sistema"; "System" = "Sistema";
+70 -34
View File
@@ -479,7 +479,7 @@
"All your contacts will remain connected." = "Minden ismerősével kapcsolatban marad."; "All your contacts will remain connected." = "Minden ismerősével kapcsolatban marad.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"All your contacts will remain connected. Profile update will be sent to your contacts." = "Az ismerőseivel kapcsolatban marad. A profil változtatások frissítésre kerülnek az ismerősöknél."; "All your contacts will remain connected. Profile update will be sent to your contacts." = "Az ismerőseivel kapcsolatban marad. A profil-változtatások frissítésre kerülnek az ismerősöknél.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Minden ismerőse, a beszélgetései és a fájljai biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP-közvetítő-kiszolgálóra."; "All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Minden ismerőse, a beszélgetései és a fájljai biztonságosan titkosításra kerülnek, melyek részletekben feltöltődnek a beállított XFTP-közvetítő-kiszolgálóra.";
@@ -694,15 +694,30 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Bad message ID" = "Téves üzenet ID"; "Bad message ID" = "Téves üzenet ID";
/* No comment provided by engineer. */
"Better calls" = "Továbbfejlesztett hívásélmény";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better groups" = "Javított csoportok"; "Better groups" = "Javított csoportok";
/* No comment provided by engineer. */
"Better message dates." = "Továbbfejlesztett üzenetdátumok.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better messages" = "Jobb üzenetek"; "Better messages" = "Jobb üzenetek";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better networking" = "Jobb hálózatkezelés"; "Better networking" = "Jobb hálózatkezelés";
/* No comment provided by engineer. */
"Better notifications" = "Továbbfejlesztett értesítések";
/* No comment provided by engineer. */
"Better security ✅" = "Továbbfejlesztett biztonság ✅";
/* No comment provided by engineer. */
"Better user experience" = "Továbbfejlesztett felhasználói élmény";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Black" = "Fekete"; "Black" = "Fekete";
@@ -918,7 +933,7 @@
"Chat preferences were changed." = "A csevegési beállítások megváltoztak."; "Chat preferences were changed." = "A csevegési beállítások megváltoztak.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Chat profile" = "Felhasználói profil"; "Chat profile" = "Csevegési profil";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Chat theme" = "Csevegés témája"; "Chat theme" = "Csevegés témája";
@@ -933,7 +948,7 @@
"Chinese and Spanish interface" = "Kínai és spanyol kezelőfelület"; "Chinese and Spanish interface" = "Kínai és spanyol kezelőfelület";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Choose _Migrate from another device_ on the new device and scan QR code." = "Válassza az _Átköltöztetés egy másik eszközről opciót az új eszközén és olvassa be a QR-kódot."; "Choose _Migrate from another device_ on the new device and scan QR code." = "Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközén és olvassa be a QR-kódot.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Choose file" = "Fájl kiválasztása"; "Choose file" = "Fájl kiválasztása";
@@ -1140,7 +1155,7 @@
"Connection terminated" = "Kapcsolat megszakítva"; "Connection terminated" = "Kapcsolat megszakítva";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Connection timeout" = "Kapcsolat időtúllépés"; "Connection timeout" = "Időtúllépés kapcsolódáskor";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Connection with desktop stopped" = "A kapcsolat a számítógéppel megszakadt"; "Connection with desktop stopped" = "A kapcsolat a számítógéppel megszakadt";
@@ -1292,6 +1307,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Custom time" = "Személyreszabott idő"; "Custom time" = "Személyreszabott idő";
/* No comment provided by engineer. */
"Customizable message shape." = "Testreszabható üzenetbuborékok.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Customize theme" = "Téma személyre szabása"; "Customize theme" = "Téma személyre szabása";
@@ -1323,7 +1341,7 @@
"Database ID: %d" = "Adatbázis-azonosító: %d"; "Database ID: %d" = "Adatbázis-azonosító: %d";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Database IDs and Transport isolation option." = "Adatbázis-azonosítók és átviteli izolációs beállítások."; "Database IDs and Transport isolation option." = "Adatbázis-azonosítók és átvitel-izolációs beállítások.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Database is encrypted using a random passphrase, you can change it." = "Az adatbázis egy véletlenszerű jelmondattal van titkosítva, ami megváltoztatható."; "Database is encrypted using a random passphrase, you can change it." = "Az adatbázis egy véletlenszerű jelmondattal van titkosítva, ami megváltoztatható.";
@@ -1482,6 +1500,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete old database?" = "Régi adatbázis törlése?"; "Delete old database?" = "Régi adatbázis törlése?";
/* No comment provided by engineer. */
"Delete or moderate up to 200 messages." = "Legfeljebb 200 üzenet egyszerre való törlése, vagy moderálása.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete pending connection?" = "Függőben lévő ismerőskérelem törlése?"; "Delete pending connection?" = "Függőben lévő ismerőskérelem törlése?";
@@ -1555,7 +1576,7 @@
"Detailed statistics" = "Részletes statisztikák"; "Detailed statistics" = "Részletes statisztikák";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Details" = "Részletek"; "Details" = "További részletek";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Develop" = "Fejlesztés"; "Develop" = "Fejlesztés";
@@ -1579,7 +1600,7 @@
"different migration in the app/database: %@ / %@" = "különböző átköltöztetések az alkalmazásban/adatbázisban: %@ / %@"; "different migration in the app/database: %@ / %@" = "különböző átköltöztetések az alkalmazásban/adatbázisban: %@ / %@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Different names, avatars and transport isolation." = "Különböző nevek, avatarok és átviteli izoláció."; "Different names, avatars and transport isolation." = "Különböző nevek, profilképek és átvitel-izoláció.";
/* connection level description */ /* connection level description */
"direct" = "közvetlen"; "direct" = "közvetlen";
@@ -1952,7 +1973,7 @@
"Error deleting token" = "Hiba a token törlésekor"; "Error deleting token" = "Hiba a token törlésekor";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Error deleting user profile" = "Hiba a felhasználói profil törlésekor"; "Error deleting user profile" = "Hiba a felhasználó-profil törlésekor";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Error downloading the archive" = "Hiba az archívum letöltésekor"; "Error downloading the archive" = "Hiba az archívum letöltésekor";
@@ -2021,7 +2042,7 @@
"Error saving settings" = "Hiba a beállítások mentésekor"; "Error saving settings" = "Hiba a beállítások mentésekor";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Error saving user password" = "Hiba a felhasználó jelszavának mentésekor"; "Error saving user password" = "Hiba a felhasználói jelszó mentésekor";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Error scanning code: %@" = "Hiba a kód beolvasásakor: %@"; "Error scanning code: %@" = "Hiba a kód beolvasásakor: %@";
@@ -2063,7 +2084,7 @@
"Error updating settings" = "Hiba történt a beállítások frissítésekor"; "Error updating settings" = "Hiba történt a beállítások frissítésekor";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Error updating user privacy" = "Hiba a felhasználói beállítások frissítésekor"; "Error updating user privacy" = "Hiba a felhasználói adatvédelem frissítésekor";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Error uploading the archive" = "Hiba az archívum feltöltésekor"; "Error uploading the archive" = "Hiba az archívum feltöltésekor";
@@ -2230,6 +2251,9 @@
/* alert message */ /* alert message */
"Forward messages without files?" = "Üzenetek továbbítása fájlok nélkül?"; "Forward messages without files?" = "Üzenetek továbbítása fájlok nélkül?";
/* No comment provided by engineer. */
"Forward up to 20 messages at once." = "Legfeljebb 20 üzenet egyszerre való továbbítása.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"forwarded" = "továbbított"; "forwarded" = "továbbított";
@@ -2384,7 +2408,7 @@
"Hidden profile password" = "Rejtett profiljelszó"; "Hidden profile password" = "Rejtett profiljelszó";
/* chat item action */ /* chat item action */
"Hide" = "Elrejtés"; "Hide" = "Összecsukás";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Hide app screen in the recent apps." = "Alkalmazás képernyőjének elrejtése a gyakran használt alkalmazások között."; "Hide app screen in the recent apps." = "Alkalmazás képernyőjének elrejtése a gyakran használt alkalmazások között.";
@@ -2435,7 +2459,7 @@
"If you enter your self-destruct passcode while opening the app:" = "Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő jelkódot:"; "If you enter your self-destruct passcode while opening the app:" = "Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő jelkódot:";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Ha most kell használnia a csevegést, koppintson alább az **Befejezés később** lehetőségre (az alkalmazás újraindításakor felajánlásra kerül az adatbázis átköltöztetése)."; "If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Ha most kell használnia a csevegést, koppintson alább a **Befejezés később** lehetőségre (az alkalmazás újraindításakor felajánlásra kerül az adatbázis átköltöztetése).";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Ignore" = "Mellőzés"; "Ignore" = "Mellőzés";
@@ -2470,6 +2494,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Importing archive" = "Archívum importálása"; "Importing archive" = "Archívum importálása";
/* No comment provided by engineer. */
"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Továbbfejlesztett kézbesítés, csökkentett adatforgalom-használat.\nTovábbi fejlesztések hamarosan!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Improved message delivery" = "Továbbfejlesztett üzenetkézbesítés"; "Improved message delivery" = "Továbbfejlesztett üzenetkézbesítés";
@@ -2789,7 +2816,7 @@
"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva."; "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Sokan kérdezték: *ha a SimpleX Chatnek nincsenek felhasználói azonosítói, akkor hogyan tud üzeneteket kézbesíteni?*"; "Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Sokan kérdezték: *ha a SimpleX Chatnek nincsenek felhasználó-azonosítói, akkor hogyan tud üzeneteket kézbesíteni?*";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Mark deleted for everyone" = "Jelölje meg mindenki számára töröltként"; "Mark deleted for everyone" = "Jelölje meg mindenki számára töröltként";
@@ -3199,7 +3226,7 @@
"Onion hosts will not be used." = "Onion-kiszolgálók nem lesznek használva."; "Onion hosts will not be used." = "Onion-kiszolgálók nem lesznek használva.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Csak az eszközök alkalmazásai tárolják a felhasználói profilokat, névjegyeket, csoportokat és a **2 rétegű végpontok közötti titkosítással** küldött üzeneteket."; "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Csak az eszközök alkalmazásai tárolják a felhasználó-profilokat, névjegyeket, csoportokat és a **2 rétegű végpontok közötti titkosítással** küldött üzeneteket.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Only delete conversation" = "Csak a beszélgetés törlése"; "Only delete conversation" = "Csak a beszélgetés törlése";
@@ -3265,7 +3292,7 @@
"Open Settings" = "Beállítások megnyitása"; "Open Settings" = "Beállítások megnyitása";
/* authentication reason */ /* authentication reason */
"Open user profiles" = "Felhasználói profilok megnyitása"; "Open user profiles" = "Felhasználó-profilok megnyitása";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Open-source protocol and code anybody can run the servers." = "Nyílt forráskódú protokoll és forráskód bárki üzemeltethet kiszolgálókat."; "Open-source protocol and code anybody can run the servers." = "Nyílt forráskódú protokoll és forráskód bárki üzemeltethet kiszolgálókat.";
@@ -3361,10 +3388,10 @@
"Picture-in-picture calls" = "Kép a képben hívások"; "Picture-in-picture calls" = "Kép a képben hívások";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"PING count" = "PING számláló"; "PING count" = "PING-ek száma";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"PING interval" = "PING időköze"; "PING interval" = "Időtartam a PING-ek között";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Play from the chat list." = "Lejátszás a csevegési listából."; "Play from the chat list." = "Lejátszás a csevegési listából.";
@@ -3523,10 +3550,10 @@
"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Védje IP-címét az ismerősei által kiválasztott üzenet-közvetítő-kiszolgálókkal szemben.\nEngedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben."; "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Védje IP-címét az ismerősei által kiválasztott üzenet-közvetítő-kiszolgálókkal szemben.\nEngedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Protocol timeout" = "Protokoll időtúllépés"; "Protocol timeout" = "Protokoll időtúllépése";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Protocol timeout per KB" = "Protokoll időkorlát KB-onként"; "Protocol timeout per KB" = "Protokoll időtúllépése KB-onként";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Proxied" = "Proxyzott"; "Proxied" = "Proxyzott";
@@ -3610,7 +3637,7 @@
"Received reply" = "Fogadott válaszüzenet-buborék színe"; "Received reply" = "Fogadott válaszüzenet-buborék színe";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Received total" = "Összes fogadott"; "Received total" = "Összes fogadott üzenet";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be."; "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be.";
@@ -3846,7 +3873,7 @@
"Save preferences?" = "Beállítások mentése?"; "Save preferences?" = "Beállítások mentése?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Save profile password" = "Felhasználói fiók jelszavának mentése"; "Save profile password" = "Profiljelszó mentése";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Save servers" = "Kiszolgálók mentése"; "Save servers" = "Kiszolgálók mentése";
@@ -4074,7 +4101,7 @@
"Sent reply" = "Válaszüzenet-buborék színe"; "Sent reply" = "Válaszüzenet-buborék színe";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Sent total" = "Összes elküldött"; "Sent total" = "Összes elküldött üzenet";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Sent via proxy" = "Proxyn keresztül küldve"; "Sent via proxy" = "Proxyn keresztül küldve";
@@ -4212,7 +4239,7 @@
"Show last messages" = "Szobák utolsó üzeneteinek megjelenítése a listanézetben"; "Show last messages" = "Szobák utolsó üzeneteinek megjelenítése a listanézetben";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Show message status" = "Üzenet állapot megjelenítése"; "Show message status" = "Üzenetállapot megjelenítése";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Show percentage" = "Százalék megjelenítése"; "Show percentage" = "Százalék megjelenítése";
@@ -4271,6 +4298,9 @@
/* simplex link type */ /* simplex link type */
"SimpleX one-time invitation" = "Egyszer használható SimpleX-meghívó"; "SimpleX one-time invitation" = "Egyszer használható SimpleX-meghívó";
/* No comment provided by engineer. */
"SimpleX protocols reviewed by Trail of Bits." = "A SimpleX Chat biztonsága a Trail of Bits által lett újraauditálva.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Simplified incognito mode" = "Egyszerűsített inkognitómód"; "Simplified incognito mode" = "Egyszerűsített inkognitómód";
@@ -4391,6 +4421,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Support SimpleX Chat" = "SimpleX Chat támogatása"; "Support SimpleX Chat" = "SimpleX Chat támogatása";
/* No comment provided by engineer. */
"Switch audio and video during the call." = "Hang/Videó váltása hívás közben.";
/* No comment provided by engineer. */
"Switch chat profile for 1-time invitations." = "Csevegési profilváltás az egyszer használható meghívókhoz.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"System" = "Rendszer"; "System" = "Rendszer";
@@ -4410,25 +4446,25 @@
"Tap to activate profile." = "A profil aktiválásához koppintson az ikonra."; "Tap to activate profile." = "A profil aktiválásához koppintson az ikonra.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Tap to Connect" = "Koppintson a kapcsolódáshoz"; "Tap to Connect" = "Koppintson ide a kapcsolódáshoz";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Tap to join" = "Koppintson a csatlakozáshoz"; "Tap to join" = "Koppintson ide a csatlakozáshoz";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Tap to join incognito" = "Koppintson az inkognitóban való csatlakozáshoz"; "Tap to join incognito" = "Koppintson ide az inkognitóban való csatlakozáshoz";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Tap to paste link" = "Koppintson ide a hivatkozás beillesztéséhez"; "Tap to paste link" = "Koppintson ide a hivatkozás beillesztéséhez";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Tap to scan" = "Koppintson a beolvasáshoz"; "Tap to scan" = "Koppintson ide a QR-kód beolvasáshoz";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"TCP connection" = "TCP kapcsolat"; "TCP connection" = "TCP kapcsolat";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"TCP connection timeout" = "TCP kapcsolat időtúllépés"; "TCP connection timeout" = "TCP kapcsolat időtúllépése";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"TCP_KEEPCNT" = "TCP_KEEPCNT"; "TCP_KEEPCNT" = "TCP_KEEPCNT";
@@ -4464,7 +4500,7 @@
"Thanks to the users contribute via Weblate!" = "Köszönet a felhasználóknak - hozzájárulás a Weblate-en!"; "Thanks to the users contribute via Weblate!" = "Köszönet a felhasználóknak - hozzájárulás a Weblate-en!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"The 1st platform without any user identifiers private by design." = "Az első csevegési rendszer bármiféle felhasználó azonosító nélkül - privátra lett tervezre."; "The 1st platform without any user identifiers private by design." = "Az első csevegési rendszer bármiféle felhasználó-azonosító nélkül - privátra lett tervezre.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Az alkalmazás értesíteni fogja, amikor üzeneteket vagy kapcsolatkéréseket kap beállítások megnyitása az engedélyezéshez."; "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Az alkalmazás értesíteni fogja, amikor üzeneteket vagy kapcsolatkéréseket kap beállítások megnyitása az engedélyezéshez.";
@@ -4599,7 +4635,7 @@
"To make a new connection" = "Új kapcsolat létrehozásához"; "To make a new connection" = "Új kapcsolat létrehozásához";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználói azonosítók helyett, a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, minden egyes ismerőshöz egy-egy különbözőt."; "To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Az adatvédelem érdekében, a más csevegési platformokon megszokott felhasználó-azonosítók helyett, a SimpleX csak az üzenetek sorbaállításához használ azonosítókat, minden egyes ismerőshöz egy-egy különbözőt.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"To protect timezone, image/voice files use UTC." = "Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak."; "To protect timezone, image/voice files use UTC." = "Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak.";
@@ -4638,10 +4674,10 @@
"Toolbar opacity" = "Eszköztár átlátszatlansága"; "Toolbar opacity" = "Eszköztár átlátszatlansága";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Total" = "Összesen"; "Total" = "Összes kapcsolat";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Transport isolation" = "Kapcsolat izolációs mód"; "Transport isolation" = "Átvitel-izoláció módja";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Transport sessions" = "Munkamenetek átvitele"; "Transport sessions" = "Munkamenetek átvitele";
@@ -5115,13 +5151,13 @@
"You can enable later via Settings" = "Később engedélyezheti a „Beállításokban”"; "You can enable later via Settings" = "Később engedélyezheti a „Beállításokban”";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You can enable them later via app Privacy & Security settings." = "Később engedélyezheti őket az alkalmazás „Adatvédelem és biztonság” menüben."; "You can enable them later via app Privacy & Security settings." = "Később engedélyezheti őket az alkalmazás „Adatvédelem és biztonság” menüben.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You can give another try." = "Megpróbálhatja még egyszer."; "You can give another try." = "Megpróbálhatja még egyszer.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You can hide or mute a user profile - swipe it to the right." = "Elrejtheti vagy lenémíthatja a felhasználó profiljait - csúsztassa jobbra a profilt."; "You can hide or mute a user profile - swipe it to the right." = "Elrejtheti vagy lenémíthatja a felhasználó -profiljait - csúsztassa jobbra a profilt.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You can make it visible to your SimpleX contacts via Settings." = "Láthatóvá teheti a SimpleXbeli ismerősei számára a „Beállításokban”."; "You can make it visible to your SimpleX contacts via Settings." = "Láthatóvá teheti a SimpleXbeli ismerősei számára a „Beállításokban”.";
@@ -2,7 +2,7 @@
"CFBundleName" = "SimpleX"; "CFBundleName" = "SimpleX";
/* Privacy - Camera Usage Description */ /* Privacy - Camera Usage Description */
"NSCameraUsageDescription" = "A SimpleX-nek kamera-hozzáférésre van szüksége a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz."; "NSCameraUsageDescription" = "A SimpleXnek kamera-hozzáférésre van szüksége a QR-kódok beolvasásához, hogy kapcsolódhasson más felhasználókhoz és videohívásokhoz.";
/* Privacy - Face ID Usage Description */ /* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "A SimpleX Face ID-t használ a helyi hitelesítéshez"; "NSFaceIDUsageDescription" = "A SimpleX Face ID-t használ a helyi hitelesítéshez";
@@ -11,8 +11,8 @@
"NSLocalNetworkUsageDescription" = "A SimpleX helyi hálózati hozzáférést használ, hogy lehetővé tegye a felhasználói csevegőprofil használatát számítógépen keresztül ugyanazon a hálózaton."; "NSLocalNetworkUsageDescription" = "A SimpleX helyi hálózati hozzáférést használ, hogy lehetővé tegye a felhasználói csevegőprofil használatát számítógépen keresztül ugyanazon a hálózaton.";
/* Privacy - Microphone Usage Description */ /* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "A SimpleX-nek mikrofon-hozzáférésre van szüksége hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez."; "NSMicrophoneUsageDescription" = "A SimpleXnek mikrofon-hozzáférésre van szüksége hang- és videohívásokhoz, valamint hangüzenetek rögzítéséhez.";
/* Privacy - Photo Library Additions Usage Description */ /* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "A SimpleX-nek hozzáférésre van szüksége a Galériához a rögzített és fogadott média mentéséhez"; "NSPhotoLibraryAddUsageDescription" = "A SimpleXnek galéria-hozzáférésre van szüksége a rögzített és fogadott média mentéséhez";
+36
View File
@@ -694,15 +694,30 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Bad message ID" = "ID del messaggio errato"; "Bad message ID" = "ID del messaggio errato";
/* No comment provided by engineer. */
"Better calls" = "Chiamate migliorate";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better groups" = "Gruppi migliorati"; "Better groups" = "Gruppi migliorati";
/* No comment provided by engineer. */
"Better message dates." = "Date dei messaggi migliorate.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better messages" = "Messaggi migliorati"; "Better messages" = "Messaggi migliorati";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better networking" = "Rete migliorata"; "Better networking" = "Rete migliorata";
/* No comment provided by engineer. */
"Better notifications" = "Notifiche migliorate";
/* No comment provided by engineer. */
"Better security ✅" = "Sicurezza migliorata ✅";
/* No comment provided by engineer. */
"Better user experience" = "Esperienza utente migliorata";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Black" = "Nero"; "Black" = "Nero";
@@ -1292,6 +1307,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Custom time" = "Tempo personalizzato"; "Custom time" = "Tempo personalizzato";
/* No comment provided by engineer. */
"Customizable message shape." = "Forma dei messaggi personalizzabile.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Customize theme" = "Personalizza il tema"; "Customize theme" = "Personalizza il tema";
@@ -1482,6 +1500,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete old database?" = "Eliminare il database vecchio?"; "Delete old database?" = "Eliminare il database vecchio?";
/* No comment provided by engineer. */
"Delete or moderate up to 200 messages." = "Elimina o modera fino a 200 messaggi.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete pending connection?" = "Eliminare la connessione in attesa?"; "Delete pending connection?" = "Eliminare la connessione in attesa?";
@@ -2230,6 +2251,9 @@
/* alert message */ /* alert message */
"Forward messages without files?" = "Inoltrare i messaggi senza file?"; "Forward messages without files?" = "Inoltrare i messaggi senza file?";
/* No comment provided by engineer. */
"Forward up to 20 messages at once." = "Inoltra fino a 20 messaggi alla volta.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"forwarded" = "inoltrato"; "forwarded" = "inoltrato";
@@ -2470,6 +2494,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Importing archive" = "Importazione archivio"; "Importing archive" = "Importazione archivio";
/* No comment provided by engineer. */
"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Consegna migliorata, utilizzo di traffico ridotto.\nAltri miglioramenti sono in arrivo!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Improved message delivery" = "Consegna dei messaggi migliorata"; "Improved message delivery" = "Consegna dei messaggi migliorata";
@@ -4271,6 +4298,9 @@
/* simplex link type */ /* simplex link type */
"SimpleX one-time invitation" = "Invito SimpleX una tantum"; "SimpleX one-time invitation" = "Invito SimpleX una tantum";
/* No comment provided by engineer. */
"SimpleX protocols reviewed by Trail of Bits." = "Protocolli di SimpleX esaminati da Trail of Bits.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Simplified incognito mode" = "Modalità incognito semplificata"; "Simplified incognito mode" = "Modalità incognito semplificata";
@@ -4391,6 +4421,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Support SimpleX Chat" = "Supporta SimpleX Chat"; "Support SimpleX Chat" = "Supporta SimpleX Chat";
/* No comment provided by engineer. */
"Switch audio and video during the call." = "Cambia tra audio e video durante la chiamata.";
/* No comment provided by engineer. */
"Switch chat profile for 1-time invitations." = "Cambia profilo di chat per inviti una tantum.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"System" = "Sistema"; "System" = "Sistema";
+6
View File
@@ -2067,6 +2067,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Messages & files" = "メッセージ & ファイル"; "Messages & files" = "メッセージ & ファイル";
/* No comment provided by engineer. */
"Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "メッセージ、ファイル、通話は、前方秘匿性、否認可能性および侵入復元性を備えた**エンドツーエンドの暗号化**によって保護されます。";
/* No comment provided by engineer. */
"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "メッセージ、ファイル、通話は、前方秘匿性、否認可能性および侵入復元性を備えた**耐量子E2E暗号化**によって保護されます。";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Migrating database archive…" = "データベースのアーカイブを移行しています…"; "Migrating database archive…" = "データベースのアーカイブを移行しています…";
+36
View File
@@ -694,15 +694,30 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Bad message ID" = "Onjuiste bericht-ID"; "Bad message ID" = "Onjuiste bericht-ID";
/* No comment provided by engineer. */
"Better calls" = "Betere gesprekken";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better groups" = "Betere groepen"; "Better groups" = "Betere groepen";
/* No comment provided by engineer. */
"Better message dates." = "Betere datums voor berichten.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better messages" = "Betere berichten"; "Better messages" = "Betere berichten";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better networking" = "Beter netwerk"; "Better networking" = "Beter netwerk";
/* No comment provided by engineer. */
"Better notifications" = "Betere meldingen";
/* No comment provided by engineer. */
"Better security ✅" = "Betere beveiliging ✅";
/* No comment provided by engineer. */
"Better user experience" = "Betere gebruikerservaring";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Black" = "Zwart"; "Black" = "Zwart";
@@ -1292,6 +1307,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Custom time" = "Aangepaste tijd"; "Custom time" = "Aangepaste tijd";
/* No comment provided by engineer. */
"Customizable message shape." = "Aanpasbare berichtvorm.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Customize theme" = "Thema aanpassen"; "Customize theme" = "Thema aanpassen";
@@ -1482,6 +1500,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete old database?" = "Oude database verwijderen?"; "Delete old database?" = "Oude database verwijderen?";
/* No comment provided by engineer. */
"Delete or moderate up to 200 messages." = "Maximaal 200 berichten verwijderen of modereren.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete pending connection?" = "Wachtende verbinding verwijderen?"; "Delete pending connection?" = "Wachtende verbinding verwijderen?";
@@ -2230,6 +2251,9 @@
/* alert message */ /* alert message */
"Forward messages without files?" = "Berichten doorsturen zonder bestanden?"; "Forward messages without files?" = "Berichten doorsturen zonder bestanden?";
/* No comment provided by engineer. */
"Forward up to 20 messages at once." = "Stuur maximaal 20 berichten tegelijk door.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"forwarded" = "doorgestuurd"; "forwarded" = "doorgestuurd";
@@ -2470,6 +2494,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Importing archive" = "Archief importeren"; "Importing archive" = "Archief importeren";
/* No comment provided by engineer. */
"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Verbeterde levering, minder data gebruik.\nBinnenkort meer verbeteringen!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Improved message delivery" = "Verbeterde berichtbezorging"; "Improved message delivery" = "Verbeterde berichtbezorging";
@@ -4271,6 +4298,9 @@
/* simplex link type */ /* simplex link type */
"SimpleX one-time invitation" = "Eenmalige SimpleX uitnodiging"; "SimpleX one-time invitation" = "Eenmalige SimpleX uitnodiging";
/* No comment provided by engineer. */
"SimpleX protocols reviewed by Trail of Bits." = "SimpleX-protocollen beoordeeld door Trail of Bits.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Simplified incognito mode" = "Vereenvoudigde incognitomodus"; "Simplified incognito mode" = "Vereenvoudigde incognitomodus";
@@ -4391,6 +4421,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Support SimpleX Chat" = "Ondersteuning van SimpleX Chat"; "Support SimpleX Chat" = "Ondersteuning van SimpleX Chat";
/* No comment provided by engineer. */
"Switch audio and video during the call." = "Wisselen tussen audio en video tijdens het gesprek.";
/* No comment provided by engineer. */
"Switch chat profile for 1-time invitations." = "Wijzig chatprofiel voor eenmalige uitnodigingen.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"System" = "Systeem"; "System" = "Systeem";
+39
View File
@@ -160,6 +160,9 @@
/* notification title */ /* notification title */
"%@ wants to connect!" = "%@ хочет соединиться!"; "%@ wants to connect!" = "%@ хочет соединиться!";
/* format for date separator in chat */
"%@, %@" = "%1$@, %2$@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"%@, %@ and %lld members" = "%@, %@ и %lld членов группы"; "%@, %@ and %lld members" = "%@, %@ и %lld членов группы";
@@ -691,15 +694,30 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Bad message ID" = "Ошибка ID сообщения"; "Bad message ID" = "Ошибка ID сообщения";
/* No comment provided by engineer. */
"Better calls" = "Улучшенные звонки";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better groups" = "Улучшенные группы"; "Better groups" = "Улучшенные группы";
/* No comment provided by engineer. */
"Better message dates." = "Улучшенные даты сообщений.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better messages" = "Улучшенные сообщения"; "Better messages" = "Улучшенные сообщения";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better networking" = "Улучшенные сетевые функции"; "Better networking" = "Улучшенные сетевые функции";
/* No comment provided by engineer. */
"Better notifications" = "Улучшенные уведомления";
/* No comment provided by engineer. */
"Better security ✅" = "Улучшенная безопасность ✅";
/* No comment provided by engineer. */
"Better user experience" = "Улучшенный интерфейс";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Black" = "Черная"; "Black" = "Черная";
@@ -1289,6 +1307,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Custom time" = "Пользовательское время"; "Custom time" = "Пользовательское время";
/* No comment provided by engineer. */
"Customizable message shape." = "Настраиваемая форма сообщений.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Customize theme" = "Настроить тему"; "Customize theme" = "Настроить тему";
@@ -1479,6 +1500,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete old database?" = "Удалить предыдущую версию данных?"; "Delete old database?" = "Удалить предыдущую версию данных?";
/* No comment provided by engineer. */
"Delete or moderate up to 200 messages." = "Удаляйте или модерируйте до 200 сообщений.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete pending connection?" = "Удалить ожидаемое соединение?"; "Delete pending connection?" = "Удалить ожидаемое соединение?";
@@ -2227,6 +2251,9 @@
/* alert message */ /* alert message */
"Forward messages without files?" = "Переслать сообщения без файлов?"; "Forward messages without files?" = "Переслать сообщения без файлов?";
/* No comment provided by engineer. */
"Forward up to 20 messages at once." = "Пересылайте до 20 сообщений за раз.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"forwarded" = "переслано"; "forwarded" = "переслано";
@@ -2467,6 +2494,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Importing archive" = "Импорт архива"; "Importing archive" = "Импорт архива";
/* No comment provided by engineer. */
"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "Улучшенная доставка, меньше трафик.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Improved message delivery" = "Улучшенная доставка сообщений"; "Improved message delivery" = "Улучшенная доставка сообщений";
@@ -4268,6 +4298,9 @@
/* simplex link type */ /* simplex link type */
"SimpleX one-time invitation" = "SimpleX одноразовая ссылка"; "SimpleX one-time invitation" = "SimpleX одноразовая ссылка";
/* No comment provided by engineer. */
"SimpleX protocols reviewed by Trail of Bits." = "Аудит SimpleX протоколов от Trail of Bits.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Simplified incognito mode" = "Упрощенный режим Инкогнито"; "Simplified incognito mode" = "Упрощенный режим Инкогнито";
@@ -4388,6 +4421,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Support SimpleX Chat" = "Поддержать SimpleX Chat"; "Support SimpleX Chat" = "Поддержать SimpleX Chat";
/* No comment provided by engineer. */
"Switch audio and video during the call." = "Переключайте звук и видео во время звонка.";
/* No comment provided by engineer. */
"Switch chat profile for 1-time invitations." = "Переключайте профиль чата для одноразовых приглашений.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"System" = "Системная"; "System" = "Системная";
+36
View File
@@ -694,15 +694,30 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Bad message ID" = "Kötü mesaj kimliği"; "Bad message ID" = "Kötü mesaj kimliği";
/* No comment provided by engineer. */
"Better calls" = "Daha iyi aramalar";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better groups" = "Daha iyi gruplar"; "Better groups" = "Daha iyi gruplar";
/* No comment provided by engineer. */
"Better message dates." = "Daha iyi mesaj tarihleri.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better messages" = "Daha iyi mesajlar"; "Better messages" = "Daha iyi mesajlar";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Better networking" = "Daha iyi ağ oluşturma"; "Better networking" = "Daha iyi ağ oluşturma";
/* No comment provided by engineer. */
"Better notifications" = "Daha iyi bildirimler";
/* No comment provided by engineer. */
"Better security ✅" = "Daha iyi güvenlik ✅";
/* No comment provided by engineer. */
"Better user experience" = "Daha iyi kullanıcı deneyimi";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Black" = "Siyah"; "Black" = "Siyah";
@@ -1292,6 +1307,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Custom time" = "Özel saat"; "Custom time" = "Özel saat";
/* No comment provided by engineer. */
"Customizable message shape." = "Özelleştirilebilir mesaj şekli.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Customize theme" = "Renk temalarını kişiselleştir"; "Customize theme" = "Renk temalarını kişiselleştir";
@@ -1482,6 +1500,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete old database?" = "Eski veritabanı silinsin mi?"; "Delete old database?" = "Eski veritabanı silinsin mi?";
/* No comment provided by engineer. */
"Delete or moderate up to 200 messages." = "200'e kadar mesajı silin veya düzenleyin.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Delete pending connection?" = "Bekleyen bağlantı silinsin mi?"; "Delete pending connection?" = "Bekleyen bağlantı silinsin mi?";
@@ -2230,6 +2251,9 @@
/* alert message */ /* alert message */
"Forward messages without files?" = "Mesajlar dosyalar olmadan iletilsin mi ?"; "Forward messages without files?" = "Mesajlar dosyalar olmadan iletilsin mi ?";
/* No comment provided by engineer. */
"Forward up to 20 messages at once." = "Aynı anda en fazla 20 mesaj iletin.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"forwarded" = "iletildi"; "forwarded" = "iletildi";
@@ -2470,6 +2494,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Importing archive" = "Arşiv içe aktarılıyor"; "Importing archive" = "Arşiv içe aktarılıyor";
/* No comment provided by engineer. */
"Improved delivery, reduced traffic usage.\nMore improvements are coming soon!" = "İyileştirilmiş teslimat, azaltılmış trafik kullanımı.\nDaha fazla iyileştirme yakında geliyor!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Improved message delivery" = "İyileştirilmiş mesaj iletimi"; "Improved message delivery" = "İyileştirilmiş mesaj iletimi";
@@ -4271,6 +4298,9 @@
/* simplex link type */ /* simplex link type */
"SimpleX one-time invitation" = "SimpleX tek kullanımlık davet"; "SimpleX one-time invitation" = "SimpleX tek kullanımlık davet";
/* No comment provided by engineer. */
"SimpleX protocols reviewed by Trail of Bits." = "SimpleX protokolleri Trail of Bits tarafından incelenmiştir.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Simplified incognito mode" = "Basitleştirilmiş gizli mod"; "Simplified incognito mode" = "Basitleştirilmiş gizli mod";
@@ -4391,6 +4421,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Support SimpleX Chat" = "SimpleX Chat'e destek ol"; "Support SimpleX Chat" = "SimpleX Chat'e destek ol";
/* No comment provided by engineer. */
"Switch audio and video during the call." = "Görüşme sırasında ses ve görüntüyü değiştirin.";
/* No comment provided by engineer. */
"Switch chat profile for 1-time invitations." = "Sohbet profilini 1 kerelik davetler için değiştirin.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"System" = "Sistem"; "System" = "Sistem";
@@ -4,8 +4,10 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.* import android.os.*
import android.view.View
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager
@@ -13,7 +15,6 @@ import chat.simplex.app.model.NtfManager.getUserIdFromIntent
import chat.simplex.common.* import chat.simplex.common.*
import chat.simplex.common.helpers.* import chat.simplex.common.helpers.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@@ -24,13 +25,21 @@ import kotlinx.coroutines.*
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
class MainActivity: FragmentActivity() { class MainActivity: FragmentActivity() {
companion object {
const val OLD_ANDROID_UI_FLAGS = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
mainActivity = WeakReference(this) mainActivity = WeakReference(this)
platform.androidSetNightModeIfSupported() platform.androidSetNightModeIfSupported()
val c = CurrentColors.value.colors val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight)
applyAppLocale(ChatModel.controller.appPrefs.appLanguage) applyAppLocale(ChatModel.controller.appPrefs.appLanguage)
// This flag makes status bar and navigation bar fully transparent. But on API level < 30 it breaks insets entirely
// https://issuetracker.google.com/issues/236862874
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// testJson() // testJson()
// When call ended and orientation changes, it re-process old intent, it's unneeded. // When call ended and orientation changes, it re-process old intent, it's unneeded.
@@ -47,6 +56,7 @@ class MainActivity: FragmentActivity() {
WindowManager.LayoutParams.FLAG_SECURE WindowManager.LayoutParams.FLAG_SECURE
) )
} }
enableEdgeToEdge()
setContent { setContent {
AppScreen() AppScreen()
} }
@@ -7,6 +7,7 @@ import chat.simplex.common.platform.Log
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.os.* import android.os.*
import android.view.View
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -16,6 +17,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.lifecycle.* import androidx.lifecycle.*
import androidx.work.* import androidx.work.*
import chat.simplex.app.MainActivity.Companion.OLD_ANDROID_UI_FLAGS
import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.app.views.call.CallActivity import chat.simplex.app.views.call.CallActivity
@@ -26,7 +28,6 @@ import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
import chat.simplex.common.views.chatlist.statusBarColorAfterCall
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.onboarding.OnboardingStage
import com.jakewharton.processphoenix.ProcessPhoenix import com.jakewharton.processphoenix.ProcessPhoenix
@@ -274,79 +275,32 @@ class SimplexApp: Application(), LifecycleEventObserver {
uiModeManager.setApplicationNightMode(mode) uiModeManager.setApplicationNightMode(mode)
} }
override fun androidSetDrawerStatusAndNavBarColor( override fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean, themeBackgroundColor: Color) {
isLight: Boolean,
drawerShadingColor: Color,
toolbarOnTop: Boolean,
navBarColor: Color,
) {
val window = mainActivity.get()?.window ?: return
@Suppress("DEPRECATION")
val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView)
// Blend status bar color to the animated color
val colors = CurrentColors.value.colors
val baseBackgroundColor = if (toolbarOnTop) colors.background.mixWith(colors.onBackground, 0.97f) else colors.background
var statusBar = baseBackgroundColor.mixWith(drawerShadingColor.copy(1f), 1 - drawerShadingColor.alpha).toArgb()
var statusBarLight = isLight
// SimplexGreen while in call
if (window.statusBarColor == SimplexGreen.toArgb()) {
statusBarColorAfterCall.intValue = statusBar
statusBar = SimplexGreen.toArgb()
statusBarLight = false
}
window.statusBarColor = statusBar
val navBar = navBarColor.toArgb()
if (windowInsetController?.isAppearanceLightStatusBars != statusBarLight) {
windowInsetController?.isAppearanceLightStatusBars = statusBarLight
}
if (window.navigationBarColor != navBar) {
window.navigationBarColor = navBar
}
if (windowInsetController?.isAppearanceLightNavigationBars != isLight) {
windowInsetController?.isAppearanceLightNavigationBars = isLight
}
}
override fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {
val window = mainActivity.get()?.window ?: return val window = mainActivity.get()?.window ?: return
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val statusLight = isLightStatusBar && chatModel.activeCall.value == null
val navBarLight = isLightNavBar || windowOrientation() == WindowOrientation.LANDSCAPE
val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView) val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView)
if (windowInsetController?.isAppearanceLightStatusBars != statusLight) {
var statusBar = (if (hasTop && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { windowInsetController?.isAppearanceLightStatusBars = statusLight
backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f) }
window.navigationBarColor = Color.Transparent.toArgb()
if (windowInsetController?.isAppearanceLightNavigationBars != navBarLight) {
windowInsetController?.isAppearanceLightNavigationBars = navBarLight
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
window.decorView.systemUiVisibility = if (statusLight && navBarLight) {
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or OLD_ANDROID_UI_FLAGS
} else if (statusLight) {
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or OLD_ANDROID_UI_FLAGS
} else if (navBarLight) {
View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR or OLD_ANDROID_UI_FLAGS
} else { } else {
if (CurrentColors.value.base == DefaultTheme.SIMPLEX) { OLD_ANDROID_UI_FLAGS
backgroundColor.lighter(0.4f) }
window.navigationBarColor = if (blackNavBar) Color.Black.toArgb() else themeBackgroundColor.toArgb()
} else { } else {
backgroundColor window.navigationBarColor = Color.Transparent.toArgb()
}
}).toArgb()
var statusBarLight = isLight
// SimplexGreen while in call
if (window.statusBarColor == SimplexGreen.toArgb()) {
statusBarColorAfterCall.intValue = statusBar
statusBar = SimplexGreen.toArgb()
statusBarLight = false
}
val navBar = (if (hasBottom && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f)
} else {
backgroundColor
}).toArgb()
if (window.statusBarColor != statusBar) {
window.statusBarColor = statusBar
}
if (windowInsetController?.isAppearanceLightStatusBars != statusBarLight) {
windowInsetController?.isAppearanceLightStatusBars = statusBarLight
}
if (window.navigationBarColor != navBar) {
window.navigationBarColor = navBar
}
if (windowInsetController?.isAppearanceLightNavigationBars != isLight) {
windowInsetController?.isAppearanceLightNavigationBars = isLight
} }
} }
@@ -401,6 +355,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
} }
return true return true
} }
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
} }
} }
} }
@@ -24,8 +24,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.*
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -37,7 +36,9 @@ import chat.simplex.app.R
import chat.simplex.app.TAG import chat.simplex.app.TAG
import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.common.helpers.applyAppLocale
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.platform.chatModel import chat.simplex.common.platform.chatModel
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
@@ -49,6 +50,7 @@ import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.*
import chat.simplex.common.platform.chatModel as m import chat.simplex.common.platform.chatModel as m
class CallActivity: ComponentActivity(), ServiceConnection { class CallActivity: ComponentActivity(), ServiceConnection {
@@ -56,6 +58,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
var boundService: CallService? = null var boundService: CallService? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
applyAppLocale(appPrefs.appLanguage)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
callActivity = WeakReference(this) callActivity = WeakReference(this)
when (intent?.action) { when (intent?.action) {
@@ -80,6 +83,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
(mainActivity.get() ?: this).applyAppLocale(appPrefs.appLanguage)
if (isOnLockScreenNow()) { if (isOnLockScreenNow()) {
lockAfterIncomingCall() lockAfterIncomingCall()
} }
@@ -233,7 +237,7 @@ fun CallActivityView() {
} }
SimpleXTheme { SimpleXTheme {
var prevCall by remember { mutableStateOf(call) } var prevCall by remember { mutableStateOf(call) }
KeyChangeEffect(m.activeCall.value) { KeyChangeEffect(m.activeCall.value, remember { appPrefs.appLanguage.state }.value) {
if (m.activeCall.value != null) { if (m.activeCall.value != null) {
prevCall = m.activeCall.value prevCall = m.activeCall.value
activity.boundService?.updateNotification() activity.boundService?.updateNotification()
@@ -64,7 +64,6 @@ kotlin {
implementation("androidx.activity:activity-compose:1.9.1") implementation("androidx.activity:activity-compose:1.9.1")
val workVersion = "2.9.1" val workVersion = "2.9.1"
implementation("androidx.work:work-runtime-ktx:$workVersion") implementation("androidx.work:work-runtime-ktx:$workVersion")
implementation("com.google.accompanist:accompanist-insets:0.30.1")
// Video support // Video support
implementation("com.google.android.exoplayer:exoplayer:2.19.1") implementation("com.google.android.exoplayer:exoplayer:2.19.1")
@@ -31,8 +31,7 @@ private fun Activity.applyLocale(locale: Locale) {
Locale.setDefault(locale) Locale.setDefault(locale)
val appConf = Configuration(androidAppContext.resources.configuration).apply { setLocale(locale) } val appConf = Configuration(androidAppContext.resources.configuration).apply { setLocale(locale) }
val activityConf = Configuration(resources.configuration).apply { setLocale(locale) } val activityConf = Configuration(resources.configuration).apply { setLocale(locale) }
@Suppress("DEPRECATION") androidAppContext = androidAppContext.createConfigurationContext(appConf)
androidAppContext.resources.updateConfiguration(appConf, resources.displayMetrics)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
resources.updateConfiguration(activityConf, resources.displayMetrics) resources.updateConfiguration(activityConf, resources.displayMetrics)
} }
@@ -3,25 +3,13 @@ package chat.simplex.common.platform
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import com.google.accompanist.insets.navigationBarsWithImePadding
import java.io.File import java.io.File
actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding()
@Composable
actual fun ProvideWindowInsets(
consumeWindowInsets: Boolean,
windowInsetsAnimationsEnabled: Boolean,
content: @Composable () -> Unit
) {
com.google.accompanist.insets.ProvideWindowInsets(content = content)
}
@Composable @Composable
actual fun Modifier.desktopOnExternalDrag( actual fun Modifier.desktopOnExternalDrag(
enabled: Boolean, enabled: Boolean,
onFiles: (List<File>) -> Unit, onFiles: (List<File>) -> Unit,
onImage: (Painter) -> Unit, onImage: (File) -> Unit,
onText: (String) -> Unit onText: (String) -> Unit
): Modifier = this ): Modifier = this
@@ -6,8 +6,7 @@ import android.graphics.drawable.ColorDrawable
import android.os.Build import android.os.Build
import android.text.InputType import android.text.InputType
import android.util.Log import android.util.Log
import android.view.OnReceiveContentListener import android.view.*
import android.view.ViewGroup
import android.view.inputmethod.* import android.view.inputmethod.*
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
@@ -26,6 +25,7 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.children
import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
@@ -94,8 +94,8 @@ actual fun PlatformTextField(
} }
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
AndroidView(modifier = Modifier, factory = { AndroidView(modifier = Modifier, factory = { context ->
val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) { val editText = @SuppressLint("AppCompatCustomView") object: EditText(context) {
override fun setOnReceiveContentListener( override fun setOnReceiveContentListener(
mimeTypes: Array<out String>?, mimeTypes: Array<out String>?,
listener: OnReceiveContentListener? listener: OnReceiveContentListener?
@@ -140,6 +140,13 @@ actual fun PlatformTextField(
Log.e(TAG, e.stackTraceToString()) Log.e(TAG, e.stackTraceToString())
} }
} }
editText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
// shows keyboard when user had search field on ChatView focused before clicking on this text field
// it still produce weird animation of closing/opening keyboard but the solution is to replace this Android EditText with Compose BasicTextField
if (hasFocus) {
showKeyboard = true
}
}
editText.doOnTextChanged { text, _, _, _ -> editText.doOnTextChanged { text, _, _, _ ->
if (!composeState.value.inProgress) { if (!composeState.value.inProgress) {
onMessageChange(text.toString()) onMessageChange(text.toString())
@@ -148,8 +155,12 @@ actual fun PlatformTextField(
} }
} }
editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") } editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") }
editText val workaround = WorkaroundFocusSearchLayout(context)
workaround.addView(editText)
workaround.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
workaround
}) { }) {
val it = it.children.first() as EditText
it.setTextColor(textColor.toArgb()) it.setTextColor(textColor.toArgb())
it.setHintTextColor(hintColor.toArgb()) it.setHintTextColor(hintColor.toArgb())
it.hint = placeholder it.hint = placeholder
@@ -6,11 +6,11 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.text.BidiFormatter import android.text.BidiFormatter
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -50,7 +50,11 @@ actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.re
} }
@Composable @Composable
actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp actual fun windowWidth(): Dp {
val direction = LocalLayoutDirection.current
val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues()
return LocalConfiguration.current.screenWidthDp.dp - cutout.calculateStartPadding(direction) - cutout.calculateEndPadding(direction)
}
@Composable @Composable
actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp
@@ -7,10 +7,12 @@ import androidx.compose.foundation.lazy.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.chatlist.NavigationBarBackground
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@@ -25,25 +27,74 @@ actual fun LazyColumnWithScrollBar(
horizontalAlignment: Alignment.Horizontal, horizontalAlignment: Alignment.Horizontal,
flingBehavior: FlingBehavior, flingBehavior: FlingBehavior,
userScrollEnabled: Boolean, userScrollEnabled: Boolean,
additionalBarOffset: State<Dp>?,
fillMaxSize: Boolean,
content: LazyListScope.() -> Unit content: LazyListScope.() -> Unit
) { ) {
val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState() val handler = LocalAppBarHandler.current
val connection = LocalAppBarHandler.current?.connection require(handler != null) { "Using LazyColumnWithScrollBar and without AppBarHandler is an error. Use LazyColumnWithScrollBarNoAppBar instead" }
val state = state ?: handler.listState
val connection = handler.connection
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (reverseLayout) {
snapshotFlow { state.layoutInfo.visibleItemsInfo.lastOrNull()?.offset ?: 0 }
.collect { scrollPosition ->
connection.appBarOffset = if (state.layoutInfo.visibleItemsInfo.lastOrNull()?.index == state.layoutInfo.totalItemsCount - 1) {
state.layoutInfo.viewportEndOffset - scrollPosition.toFloat() - state.layoutInfo.afterContentPadding
} else {
// show always when last item is not visible
-1000f
}
//Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
}
} else {
snapshotFlow { state.firstVisibleItemScrollOffset } snapshotFlow { state.firstVisibleItemScrollOffset }
.filter { state.firstVisibleItemIndex == 0 } .filter { state.firstVisibleItemIndex == 0 }
.collect { scrollPosition -> .collect { scrollPosition ->
val offset = connection?.appBarOffset val offset = connection.appBarOffset
if (offset != null && (offset + scrollPosition).absoluteValue > 1) { if ((offset + scrollPosition + state.layoutInfo.afterContentPadding).absoluteValue > 1) {
connection.appBarOffset = -scrollPosition.toFloat() connection.appBarOffset = -scrollPosition.toFloat()
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") //Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
} }
} }
} }
if (connection != null) { }
LazyColumn(modifier.nestedScroll(connection), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) LazyColumn(
if (fillMaxSize) {
Modifier.fillMaxSize().copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection)
} else { } else {
LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection)
},
state,
contentPadding,
reverseLayout,
verticalArrangement,
horizontalAlignment,
flingBehavior,
userScrollEnabled
) {
content()
}
}
@Composable
actual fun LazyColumnWithScrollBarNoAppBar(
modifier: Modifier,
state: LazyListState?,
contentPadding: PaddingValues,
reverseLayout: Boolean,
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal,
flingBehavior: FlingBehavior,
userScrollEnabled: Boolean,
additionalBarOffset: State<Dp>?,
content: LazyListScope.() -> Unit
) {
val state = state ?: rememberLazyListState()
LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled) {
content()
} }
} }
@@ -54,32 +105,80 @@ actual fun ColumnWithScrollBar(
horizontalAlignment: Alignment.Horizontal, horizontalAlignment: Alignment.Horizontal,
state: ScrollState?, state: ScrollState?,
maxIntrinsicSize: Boolean, maxIntrinsicSize: Boolean,
fillMaxSize: Boolean,
content: @Composable() (ColumnScope.() -> Unit) content: @Composable() (ColumnScope.() -> Unit)
) { ) {
val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState() val handler = LocalAppBarHandler.current
val connection = LocalAppBarHandler.current?.connection require(handler != null) { "Using ColumnWithScrollBar and without AppBarHandler is an error. Use ColumnWithScrollBarNoAppBar instead" }
val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier).imePadding() else modifier.imePadding()
val state = state ?: handler.scrollState
val connection = handler.connection
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
snapshotFlow { state.value } snapshotFlow { state.value }
.collect { scrollPosition -> .collect { scrollPosition ->
val offset = connection?.appBarOffset val offset = connection.appBarOffset
if (offset != null && (offset + scrollPosition).absoluteValue > 1) { if ((offset + scrollPosition).absoluteValue > 1) {
connection.appBarOffset = -scrollPosition.toFloat() connection.appBarOffset = -scrollPosition.toFloat()
// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") // Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}")
} }
} }
} }
if (connection != null) { val oneHandUI = remember { appPrefs.oneHandUI.state }
Box(Modifier.fillMaxHeight()) {
Column( Column(
if (maxIntrinsicSize) { if (maxIntrinsicSize) {
modifier.nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max) Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max)
} else { } else {
modifier.nestedScroll(connection).verticalScroll(state) Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).then(modifier).nestedScroll(connection).verticalScroll(state)
}, verticalArrangement, horizontalAlignment, content) }, verticalArrangement, horizontalAlignment
) {
if (oneHandUI.value) {
Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp).windowInsetsTopHeight(WindowInsets.statusBars))
content()
Spacer(Modifier.navigationBarsPadding().padding(bottom = AppBarHeight * fontSizeSqrtMultiplier))
} else { } else {
Column(if (maxIntrinsicSize) { Spacer(Modifier.statusBarsPadding().padding(top = AppBarHeight * fontSizeSqrtMultiplier))
content()
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}
}
if (!oneHandUI.value) {
NavigationBarBackground(false, false)
}
}
}
@Composable
actual fun ColumnWithScrollBarNoAppBar(
modifier: Modifier,
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal,
state: ScrollState?,
maxIntrinsicSize: Boolean,
content: @Composable() (ColumnScope.() -> Unit)
) {
val modifier = modifier.imePadding()
val state = state ?: rememberScrollState()
val oneHandUI = remember { appPrefs.oneHandUI.state }
Box(Modifier.fillMaxHeight()) {
Column(
if (maxIntrinsicSize) {
modifier.verticalScroll(state).height(IntrinsicSize.Max) modifier.verticalScroll(state).height(IntrinsicSize.Max)
} else { } else {
modifier.verticalScroll(state) modifier.verticalScroll(state)
}, verticalArrangement, horizontalAlignment, content) }, verticalArrangement, horizontalAlignment
) {
if (oneHandUI.value) {
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))
content()
} else {
content()
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}
}
if (!oneHandUI.value) {
NavigationBarBackground(false, false)
}
} }
} }
@@ -3,17 +3,18 @@ package chat.simplex.common.platform
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.graphics.Rect
import android.os.* import android.os.*
import android.view.* import android.view.*
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.ime
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import chat.simplex.common.AppScreen import chat.simplex.common.AppScreen
import chat.simplex.common.model.clear import chat.simplex.common.model.clear
import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import androidx.compose.ui.platform.LocalContext as LocalContext1 import androidx.compose.ui.platform.LocalContext as LocalContext1
import chat.simplex.res.MR import chat.simplex.res.MR
@@ -43,28 +44,13 @@ actual fun LocalMultiplatformView(): Any? = LocalView.current
@Composable @Composable
actual fun getKeyboardState(): State<KeyboardState> { actual fun getKeyboardState(): State<KeyboardState> {
val keyboardState = remember { mutableStateOf(KeyboardState.Closed) } val density = LocalDensity.current
val view = LocalView.current val ime = WindowInsets.ime
DisposableEffect(view) { return remember {
val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { derivedStateOf {
val rect = Rect() if (ime.getBottom(density) == 0) KeyboardState.Closed else KeyboardState.Opened
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
keyboardState.value = if (keypadHeight > screenHeight * 0.15) {
KeyboardState.Opened
} else {
KeyboardState.Closed
} }
} }
view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener)
}
}
return keyboardState
} }
actual fun hideKeyboard(view: Any?, clearFocus: Boolean) { actual fun hideKeyboard(view: Any?, clearFocus: Boolean) {
@@ -7,12 +7,9 @@ import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.runtime.* import androidx.compose.runtime.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.withBGApi
import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.ImageResource
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import java.util.concurrent.Executors import java.util.concurrent.Executors
interface CallAudioDeviceManagerInterface { interface CallAudioDeviceManagerInterface {
@@ -22,7 +19,6 @@ interface CallAudioDeviceManagerInterface {
fun stop() fun stop()
// AudioDeviceInfo.AudioDeviceType // AudioDeviceInfo.AudioDeviceType
fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyExternal: Boolean) fun selectLastExternalDeviceOrDefault(speaker: Boolean, keepAnyExternal: Boolean)
fun selectSameDeviceOnWebViewChange()
// AudioDeviceInfo.AudioDeviceType // AudioDeviceInfo.AudioDeviceType
fun selectDevice(id: Int) fun selectDevice(id: Int)
@@ -39,15 +35,12 @@ interface CallAudioDeviceManagerInterface {
@RequiresApi(Build.VERSION_CODES.S) @RequiresApi(Build.VERSION_CODES.S)
class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface { class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
private val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager private val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private var resetCurrentToDevice: AudioDeviceInfo? = null
private var resetCurrentToDeviceJob: Job = Job()
override val devices: MutableState<List<AudioDeviceInfo>> = mutableStateOf(emptyList()) override val devices: MutableState<List<AudioDeviceInfo>> = mutableStateOf(emptyList())
override val currentDevice: MutableState<AudioDeviceInfo?> = mutableStateOf(null) override val currentDevice: MutableState<AudioDeviceInfo?> = mutableStateOf(null)
private val audioCallback = object: AudioDeviceCallback() { private val audioCallback = object: AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) { override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}") Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
resetCurrentToDevice = null
super.onAudioDevicesAdded(addedDevices) super.onAudioDevicesAdded(addedDevices)
val oldDevices = devices.value val oldDevices = devices.value
devices.value = am.availableCommunicationDevices devices.value = am.availableCommunicationDevices
@@ -60,21 +53,13 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) { override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}") Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
resetCurrentToDevice = null
super.onAudioDevicesRemoved(removedDevices) super.onAudioDevicesRemoved(removedDevices)
devices.value = am.availableCommunicationDevices devices.value = am.availableCommunicationDevices
} }
} }
private val listener: OnCommunicationDeviceChangedListener = OnCommunicationDeviceChangedListener { device -> private val listener: OnCommunicationDeviceChangedListener = OnCommunicationDeviceChangedListener { device ->
val resetTo = resetCurrentToDevice
if (resetTo != null && device != null && resetTo.id != device.id) {
Log.w(TAG, "Resetting device that was set by WebView ${device.name?.localized()} to previously set device ${resetTo.name?.localized()}")
selectDevice(resetTo.id)
return@OnCommunicationDeviceChangedListener
}
devices.value = am.availableCommunicationDevices devices.value = am.availableCommunicationDevices
//Log.d(TAG, "Devices changed ${device?.details()} | ${devices.value.map { it.details() }}")
currentDevice.value = device currentDevice.value = device
} }
@@ -113,18 +98,7 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
} }
} }
// WebView modifies speaker when muting/unmuting microphone. It's not needed at all, returning back current device if it will be changed
override fun selectSameDeviceOnWebViewChange() {
resetCurrentToDevice = currentDevice.value
resetCurrentToDeviceJob.cancel()
resetCurrentToDeviceJob = withBGApi {
delay(5000)
resetCurrentToDevice = null
}
}
override fun selectDevice(id: Int) { override fun selectDevice(id: Int) {
resetCurrentToDevice = null
val device = devices.value.lastOrNull { it.id == id } val device = devices.value.lastOrNull { it.id == id }
if (device != null && am.communicationDevice?.id != id ) { if (device != null && am.communicationDevice?.id != id ) {
am.setCommunicationDevice(device) am.setCommunicationDevice(device)
@@ -134,14 +108,12 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface { class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
private val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager private val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private var resetCurrentToDeviceJob: Job = Job()
override val devices: MutableState<List<AudioDeviceInfo>> = mutableStateOf(emptyList()) override val devices: MutableState<List<AudioDeviceInfo>> = mutableStateOf(emptyList())
override val currentDevice: MutableState<AudioDeviceInfo?> = mutableStateOf(null) override val currentDevice: MutableState<AudioDeviceInfo?> = mutableStateOf(null)
private val audioCallback = object: AudioDeviceCallback() { private val audioCallback = object: AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) { override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}") Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
resetCurrentToDeviceJob.cancel()
super.onAudioDevicesAdded(addedDevices) super.onAudioDevicesAdded(addedDevices)
devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired() devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired()
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false) selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false)
@@ -149,7 +121,6 @@ class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) { override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}") Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
resetCurrentToDeviceJob.cancel()
super.onAudioDevicesRemoved(removedDevices) super.onAudioDevicesRemoved(removedDevices)
devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired() devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired()
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, true) selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, true)
@@ -182,22 +153,7 @@ class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
} }
} }
override fun selectSameDeviceOnWebViewChange() {
val current = currentDevice.value ?: return
resetCurrentToDeviceJob.cancel()
resetCurrentToDeviceJob = withBGApi {
// select old device manually because WebView will change it for sure.
// smaller delay first if it's possible to make the switch less noticeable
delay(300)
selectDevice(current.id)
// and in order to be sure that it will be selected even if WebView will be slow in changing its device
delay(700)
selectDevice(current.id)
}
}
override fun selectDevice(id: Int) { override fun selectDevice(id: Int) {
resetCurrentToDeviceJob.cancel()
val device = devices.value.lastOrNull { it.id == id } val device = devices.value.lastOrNull { it.id == id }
val isExternalDevice = device != null && device.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE && device.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER val isExternalDevice = device != null && device.type != AudioDeviceInfo.TYPE_BUILTIN_EARPIECE && device.type != AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
if (isExternalDevice) { if (isExternalDevice) {
@@ -260,8 +216,6 @@ val AudioDeviceInfo.icon: ImageResource
else -> MR.images.ic_brand_awareness_filled else -> MR.images.ic_brand_awareness_filled
} }
private fun AudioDeviceInfo.details(): String = "$productName id:$id name:${name?.localized()} type:$type sink:$isSink source:$isSource"
val AudioDeviceInfo.name: StringResource? val AudioDeviceInfo.name: StringResource?
get() = when (this.type) { get() = when (this.type) {
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> MR.strings.audio_device_earpiece AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> MR.strings.audio_device_earpiece
@@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
@@ -41,11 +40,14 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.* import androidx.lifecycle.*
import androidx.webkit.WebViewAssetLoader import androidx.webkit.WebViewAssetLoader
import androidx.webkit.WebViewClientCompat import androidx.webkit.WebViewClientCompat
import chat.simplex.common.helpers.applyAppLocale
import chat.simplex.common.helpers.showAllowPermissionInSettingsAlert import chat.simplex.common.helpers.showAllowPermissionInSettingsAlert
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.res.MR import chat.simplex.res.MR
import com.google.accompanist.permissions.* import com.google.accompanist.permissions.*
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
@@ -192,7 +194,11 @@ actual fun ActiveCallView() {
updateActiveCall(call) { updateActiveCall(call) {
val sources = it.localMediaSources val sources = it.localMediaSources
when (cmd.source) { when (cmd.source) {
CallMediaSource.Mic -> it.copy(localMediaSources = sources.copy(mic = cmd.enable)) CallMediaSource.Mic -> {
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
am.isMicrophoneMute = !cmd.enable
it.copy(localMediaSources = sources.copy(mic = cmd.enable))
}
CallMediaSource.Camera -> it.copy(localMediaSources = sources.copy(camera = cmd.enable)) CallMediaSource.Camera -> it.copy(localMediaSources = sources.copy(camera = cmd.enable))
CallMediaSource.ScreenAudio -> it.copy(localMediaSources = sources.copy(screenAudio = cmd.enable)) CallMediaSource.ScreenAudio -> it.copy(localMediaSources = sources.copy(screenAudio = cmd.enable))
CallMediaSource.ScreenVideo -> it.copy(localMediaSources = sources.copy(screenVideo = cmd.enable)) CallMediaSource.ScreenVideo -> it.copy(localMediaSources = sources.copy(screenVideo = cmd.enable))
@@ -202,7 +208,6 @@ actual fun ActiveCallView() {
is WCallCommand.Camera -> { is WCallCommand.Camera -> {
updateActiveCall(call) { it.copy(localCamera = cmd.camera) } updateActiveCall(call) { it.copy(localCamera = cmd.camera) }
if (!call.localMediaSources.mic) { if (!call.localMediaSources.mic) {
callAudioDeviceManager.selectSameDeviceOnWebViewChange()
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false)) chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false))
} }
} }
@@ -262,10 +267,7 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceM
devices = remember { callAudioDeviceManager.devices }.value, devices = remember { callAudioDeviceManager.devices }.value,
currentDevice = remember { callAudioDeviceManager.currentDevice }, currentDevice = remember { callAudioDeviceManager.currentDevice },
dismiss = { withBGApi { chatModel.callManager.endCall(call) } }, dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
toggleAudio = { toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) },
callAudioDeviceManager.selectSameDeviceOnWebViewChange()
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic))
},
selectDevice = { callAudioDeviceManager.selectDevice(it.id) }, selectDevice = { callAudioDeviceManager.selectDevice(it.id) },
toggleVideo = { toggleVideo = {
if (ContextCompat.checkSelfPermission(androidAppContext, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(androidAppContext, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
@@ -327,11 +329,14 @@ private fun ActiveCallOverlayLayout(
flipCamera: () -> Unit flipCamera: () -> Unit
) { ) {
Column { Column {
CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) { CallAppBar(
title = {
if (call.hasVideo) { if (call.hasVideo) {
Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1) Text(call.contact.chatViewName, Modifier.offset(x = (-4).dp).padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
}
} }
},
onBack = { chatModel.activeCallViewIsCollapsed.value = true }
)
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
@Composable @Composable
fun SelectSoundDevice(size: Dp) { fun SelectSoundDevice(size: Dp) {
@@ -409,6 +414,7 @@ private fun ControlButton(icon: Painter, iconText: StringResource, enabled: Bool
@Composable @Composable
private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, content: @Composable () -> Unit) { private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, content: @Composable () -> Unit) {
val ripple = remember { ripple(bounded = false, radius = size / 2, color = background.lighter(0.1f)) }
Box( Box(
Modifier Modifier
.background(background, CircleShape) .background(background, CircleShape)
@@ -417,7 +423,7 @@ private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, backg
onClick = action, onClick = action,
role = Role.Button, role = Role.Button,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = size / 2, color = background.lighter(0.1f)), indication = ripple,
enabled = enabled enabled = enabled
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -588,8 +594,9 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni
} }
} }
} else { } else {
ModalView(background = Color.Black, showClose = false, close = {}) { ModalView(background = Color.Black, showAppBar = false, close = {}) {
ColumnWithScrollBar(Modifier.fillMaxSize()) { Column {
Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier))
AppBarTitle(stringResource(MR.strings.permissions_required)) AppBarTitle(stringResource(MR.strings.permissions_required))
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
val onClick = { val onClick = {
@@ -706,9 +713,10 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
AndroidView( AndroidView(
factory = { AndroidViewContext -> factory = {
try { try {
(staticWebView ?: WebView(androidAppContext)).apply { (staticWebView ?: WebView(androidAppContext)).apply {
reapplyLocale()
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
@@ -779,6 +787,16 @@ private fun updateActiveCall(initial: Call, transform: (Call) -> Call) {
} }
} }
/*
* Creating WebView automatically drops user's custom app locale to default system locale.
* Preventing it by re-applying custom locale
* https://issuetracker.google.com/issues/109833940
* */
private fun reapplyLocale() {
mainActivity.get()?.applyAppLocale(appPrefs.appLanguage)
callActivity.get()?.applyAppLocale(appPrefs.appLanguage)
}
private class LocalContentWebViewClient(val webView: MutableState<WebView?>, private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() { private class LocalContentWebViewClient(val webView: MutableState<WebView?>, private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() {
override fun shouldInterceptRequest( override fun shouldInterceptRequest(
view: WebView, view: WebView,
@@ -8,7 +8,6 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import chat.simplex.common.model.CIFile import chat.simplex.common.model.CIFile
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.views.helpers.ModalManager import chat.simplex.common.views.helpers.ModalManager
@@ -39,14 +38,6 @@ actual fun SimpleAndAnimatedImageView(
if (getLoadedFilePath(file) != null) { if (getLoadedFilePath(file) != null) {
ModalManager.fullscreen.showCustomModal(animated = false) { close -> ModalManager.fullscreen.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close) ImageFullScreenView(imageProvider, close)
if (smallView) {
DisposableEffect(Unit) {
onDispose {
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
}
}
}
} }
} }
} }
@@ -1,32 +1,26 @@
package chat.simplex.common.views.chatlist package chat.simplex.common.views.chatlist
import android.app.Activity
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.sp
import chat.simplex.common.ANDROID_CALL_TOP_PADDING import chat.simplex.common.ANDROID_CALL_TOP_PADDING
import chat.simplex.common.model.durationText import chat.simplex.common.model.durationText
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp
@@ -38,11 +32,12 @@ private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM
@Composable @Composable
actual fun ActiveCallInteractiveArea(call: Call) { actual fun ActiveCallInteractiveArea(call: Call) {
val onClick = { platform.androidStartCallActivity(false) } val onClick = { platform.androidStartCallActivity(false) }
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) { val statusBar = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT + statusBar)) {
val source = remember { MutableInteractionSource() } val source = remember { MutableInteractionSource() }
val indication = rememberRipple(bounded = true, 3000.dp) val ripple = remember { ripple(bounded = true, 3000.dp) }
Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = indication, interactionSource = source)) { Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT + statusBar).clickable(onClick = onClick, indication = ripple, interactionSource = source)) {
GreenLine(call) GreenLine(statusBar, call)
} }
Box( Box(
Modifier Modifier
@@ -50,7 +45,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
.size(CALL_BOTTOM_ICON_HEIGHT) .size(CALL_BOTTOM_ICON_HEIGHT)
.background(SimplexGreen, CircleShape) .background(SimplexGreen, CircleShape)
.clip(CircleShape) .clip(CircleShape)
.clickable(onClick = onClick, indication = indication, interactionSource = source) .clickable(onClick = onClick, indication = ripple, interactionSource = source)
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@@ -63,16 +58,13 @@ actual fun ActiveCallInteractiveArea(call: Call) {
} }
} }
// Temporary solution for storing a color that needs to be applied after call ends
var statusBarColorAfterCall = mutableIntStateOf(CurrentColors.value.colors.background.toArgb())
@Composable @Composable
private fun GreenLine(call: Call) { private fun GreenLine(statusBarHeight: Dp, call: Call) {
Row( Row(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.background(SimplexGreen) .background(SimplexGreen)
.padding(top = -CALL_TOP_OFFSET) .padding(top = -CALL_TOP_OFFSET + statusBarHeight)
.padding(horizontal = DEFAULT_PADDING), .padding(horizontal = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
@@ -81,12 +73,10 @@ private fun GreenLine(call: Call) {
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
CallDuration(call) CallDuration(call)
} }
val window = (LocalContext.current as Activity).window
DisposableEffect(Unit) { DisposableEffect(Unit) {
statusBarColorAfterCall.intValue = window.statusBarColor platform.androidSetStatusAndNavigationBarAppearance(false, CurrentColors.value.colors.isLight)
window.statusBarColor = SimplexGreen.toArgb()
onDispose { onDispose {
window.statusBarColor = statusBarColorAfterCall.intValue platform.androidSetStatusAndNavigationBarAppearance(CurrentColors.value.colors.isLight, CurrentColors.value.colors.isLight)
} }
} }
} }
@@ -19,13 +19,11 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.User import chat.simplex.common.model.User
import chat.simplex.common.model.UserInfo import chat.simplex.common.model.UserInfo
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -36,6 +34,7 @@ private val USER_PICKER_ROW_PADDING = 16.dp
@Composable @Composable
actual fun UserPickerUsersSection( actual fun UserPickerUsersSection(
users: List<UserInfo>, users: List<UserInfo>,
iconColor: Color,
stopped: Boolean, stopped: Boolean,
onUserClicked: (user: User) -> Unit, onUserClicked: (user: User) -> Unit,
) { ) {
@@ -140,41 +139,15 @@ actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow<
} else { } else {
Modifier Modifier
} }
Box {
Box( Box(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.then(clickableModifier) .then(clickableModifier)
.drawBehind { .drawBehind {
val pos = when { val pos = calculatePosition(dismissState)
dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f
dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f
dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction
else -> 1 - dismissState.progress.fraction
}
val colors = CurrentColors.value.colors val colors = CurrentColors.value.colors
val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f) val resultingColor = if (colors.isLight) colors.onSurface.copy(alpha = ScrimOpacity) else Color.Black.copy(0.64f)
val adjustedAlpha = resultingColor.alpha * calculateFraction(pos = pos)
val shadingColor = resultingColor.copy(alpha = adjustedAlpha)
if (pickerState.value.isVisible()) {
platform.androidSetDrawerStatusAndNavBarColor(
isLight = colors.isLight,
drawerShadingColor = shadingColor,
toolbarOnTop = !appPrefs.oneHandUI.get(),
navBarColor = colors.background.mixWith(colors.onBackground, 1 - userPickerAlpha())
)
} else if (ModalManager.start.modalCount.value == 0) {
platform.androidSetDrawerStatusAndNavBarColor(
isLight = colors.isLight,
drawerShadingColor = shadingColor,
toolbarOnTop = !appPrefs.oneHandUI.get(),
navBarColor = (if (appPrefs.oneHandUI.get() && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
colors.background.mixWith(CurrentColors.value.colors.onBackground, 0.97f)
} else {
colors.background
})
)
}
drawRect( drawRect(
if (pos != 0f) resultingColor else Color.Transparent, if (pos != 0f) resultingColor else Color.Transparent,
alpha = calculateFraction(pos = pos) alpha = calculateFraction(pos = pos)
@@ -214,11 +187,23 @@ actual fun PlatformUserPicker(modifier: Modifier, pickerState: MutableStateFlow<
swipeDistance = height.intValue.toFloat(), swipeDistance = height.intValue.toFloat(),
) )
else Modifier else Modifier
Box(draggableModifier.then(modifier)) { Box(draggableModifier.then(modifier).navigationBarsPadding()) {
content() content()
} }
} }
} }
NavigationBarBackground(
modifier = Modifier.graphicsLayer { alpha = if (calculatePosition(dismissState) > 0.1f) 1f else 0f },
color = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, alpha = 1 - userPickerAlpha())
)
}
}
private fun calculatePosition(dismissState: DismissState): Float = when {
dismissState.progress.from == DismissValue.Default && dismissState.progress.to == DismissValue.Default -> 1f
dismissState.progress.from == DismissValue.DismissedToEnd && dismissState.progress.to == DismissValue.DismissedToEnd -> 0f
dismissState.progress.to == DismissValue.Default -> dismissState.progress.fraction
else -> 1 - dismissState.progress.fraction
} }
private fun Modifier.draggableBottomDrawerModifier( private fun Modifier.draggableBottomDrawerModifier(
@@ -171,6 +171,8 @@ actual fun GetImageBottomSheet(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
.imePadding()
.navigationBarsPadding()
.onFocusChanged { focusState -> .onFocusChanged { focusState ->
if (!focusState.hasFocus) hideBottomSheet() if (!focusState.hasFocus) hideBottomSheet()
} }
@@ -0,0 +1,41 @@
package chat.simplex.common.views.helpers
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
/**
* A workaround for the ANR issue on Compose 1.7.x.
* https://issuetracker.google.com/issues/369354336
* Code from:
* https://issuetracker.google.com/issues/369354336#comment8
*/
class WorkaroundFocusSearchLayout : FrameLayout {
constructor(
context: Context,
) : super(context)
constructor(
context: Context,
attrs: AttributeSet?,
) : super(context, attrs)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
) : super(context, attrs, defStyleAttr)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int,
) : super(context, attrs, defStyleAttr, defStyleRes)
override fun focusSearch(focused: View?, direction: Int): View? {
return null
}
}
@@ -2,6 +2,7 @@ package chat.simplex.common.views.usersettings
import SectionBottomSpacer import SectionBottomSpacer
import SectionDividerSpaced import SectionDividerSpaced
import SectionSpacer
import SectionView import SectionView
import android.app.Activity import android.app.Activity
import android.content.ComponentName import android.content.ComponentName
@@ -31,6 +32,7 @@ import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.helpers.APPLICATION_ID import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.saveAppLocale import chat.simplex.common.helpers.saveAppLocale
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
@@ -75,9 +77,7 @@ fun AppearanceScope.AppearanceLayout(
systemDarkTheme: SharedPreference<String?>, systemDarkTheme: SharedPreference<String?>,
changeIcon: (AppIcon) -> Unit, changeIcon: (AppIcon) -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.appearance_settings)) AppBarTitle(stringResource(MR.strings.appearance_settings))
SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) { SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) {
val context = LocalContext.current val context = LocalContext.current
@@ -106,15 +106,15 @@ fun AppearanceScope.AppearanceLayout(
} }
// } // }
SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) { SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI)
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false)
}
} }
SectionDividerSpaced() SectionDividerSpaced()
ThemesSection(systemDarkTheme) ThemesSection(systemDarkTheme)
SectionDividerSpaced()
AppToolbarsSection()
SectionDividerSpaced() SectionDividerSpaced()
MessageShapeSection() MessageShapeSection()
@@ -13,14 +13,13 @@ import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
actual fun SettingsSectionApp( actual fun SettingsSectionApp(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit, showVersion: () -> Unit,
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
) { ) {
SectionView(stringResource(MR.strings.settings_section_title_app)) { SectionView(stringResource(MR.strings.settings_section_title_app)) {
SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp) SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp)
SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) }) SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) })
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }) SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) })
AppVersionItem(showVersion) AppVersionItem(showVersion)
} }
} }
@@ -11,10 +11,13 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
import chat.simplex.common.model.* import chat.simplex.common.model.*
@@ -39,17 +42,42 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlin.math.absoluteValue
@Composable @Composable
fun AppScreen() { fun AppScreen() {
AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() } AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() }
SimpleXTheme { SimpleXTheme {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
// This padding applies to landscape view only taking care of navigation bar and holes in screen in status bar area
// (because nav bar and holes located on vertical sides of screen in landscape view)
val direction = LocalLayoutDirection.current
val safePadding = WindowInsets.safeDrawing.asPaddingValues()
val cutout = WindowInsets.displayCutout.asPaddingValues()
val cutoutStart = cutout.calculateStartPadding(direction)
val cutoutEnd = cutout.calculateEndPadding(direction)
val cutoutMax = maxOf(cutoutStart, cutoutEnd)
val paddingStartUntouched = safePadding.calculateStartPadding(direction)
val paddingStart = paddingStartUntouched - cutoutStart
val paddingEndUntouched = safePadding.calculateEndPadding(direction)
val paddingEnd = paddingEndUntouched - cutoutEnd
// Such a strange layout is needed because the main content should be covered by solid color in order to hide overflow
// of some elements that may have negative offset (so, can't use Row {}).
// To check: go to developer settings of Android, choose Display cutout -> Punch hole, and rotate the phone to landscape, open any chat
Box {
val fullscreenGallery = remember { chatModel.fullscreenGalleryVisible }
Box(Modifier.padding(start = paddingStart + cutoutMax, end = paddingEnd + cutoutMax).consumeWindowInsets(PaddingValues(start = paddingStartUntouched, end = paddingEndUntouched))) {
Box(Modifier.drawBehind {
if (fullscreenGallery.value) {
drawRect(Color.Black, topLeft = Offset(-(paddingStart + cutoutMax).toPx(), 0f), Size(size.width + (paddingStart + cutoutMax).toPx() + (paddingEnd + cutoutMax).toPx(), size.height))
}
}) {
MainScreen() MainScreen()
} }
} }
} }
}
}
} }
@Composable @Composable
@@ -138,7 +166,9 @@ fun MainScreen() {
} }
SetupClipboardListener() SetupClipboardListener()
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
AndroidWrapInCallLayout {
AndroidScreen(userPickerState) AndroidScreen(userPickerState)
}
} else { } else {
DesktopScreen(userPickerState) DesktopScreen(userPickerState)
} }
@@ -170,7 +200,9 @@ fun MainScreen() {
} }
} }
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
AndroidWrapInCallLayout {
ModalManager.fullscreen.showInView() ModalManager.fullscreen.showInView()
}
SwitchingUsersView() SwitchingUsersView()
} }
@@ -237,19 +269,39 @@ fun MainScreen() {
val ANDROID_CALL_TOP_PADDING = 40.dp val ANDROID_CALL_TOP_PADDING = 40.dp
@Composable
fun AndroidWrapInCallLayout(content: @Composable () -> Unit) {
val call = remember { chatModel.activeCall}.value
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
Box {
Box(Modifier.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)) {
content()
}
if (call != null && showCallArea) {
ActiveCallInteractiveArea(call)
}
}
}
@Composable @Composable
fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) { fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
BoxWithConstraints { BoxWithConstraints {
val call = remember { chatModel.activeCall} .value
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
val currentChatId = remember { mutableStateOf(chatModel.chatId.value) } val currentChatId = remember { mutableStateOf(chatModel.chatId.value) }
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) } val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues()
val direction = LocalLayoutDirection.current
val hasCutout = cutout.calculateStartPadding(direction) + cutout.calculateEndPadding(direction) > 0.dp
Box( Box(
Modifier Modifier
// clipping only for devices with cutout currently visible on sides. It prevents showing chat list with open chat view
// In order cases it's not needed to use clip
.then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier)
.graphicsLayer { .graphicsLayer {
translationX = -offset.value.dp.toPx() // minOf thing is needed for devices with holes in screen while the user on ChatView rotates his phone from portrait to landscape
// because in this case (at least in emulator) maxWidth changes in two steps: big first, smaller on next frame.
// But offset is remembered already, so this is a better way than dropping a value of offset
translationX = -minOf(offset.value.dp, maxWidth).toPx()
} }
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
) { ) {
StartPartOfScreen(userPickerState) StartPartOfScreen(userPickerState)
} }
@@ -271,52 +323,41 @@ fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
snapshotFlow { chatModel.chatId.value } snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged() .distinctUntilChanged()
.collect { .collect {
if (it == null) { if (it == null) onComposed(null)
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
onComposed(null)
}
currentChatId.value = it currentChatId.value = it
} }
} }
} }
LaunchedEffect(Unit) {
snapshotFlow { ModalManager.center.modalCount.value > 0 }
.filter { chatModel.chatId.value == null }
.collect { modalBackground ->
if (chatModel.newChatSheetVisible.value) {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, appPrefs.oneHandUI.get())
} else if (modalBackground) {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
} else {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
}
}
}
Box(Modifier Box(Modifier
.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() } .then(if (hasCutout) Modifier.clip(RectangleShape) else Modifier)
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp) .graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() }
) Box2@{ ) Box2@{
currentChatId.value?.let { currentChatId.value?.let {
ChatView(currentChatId, onComposed) ChatView(currentChatId, onComposed)
} }
} }
if (call != null && showCallArea) {
ActiveCallInteractiveArea(call)
}
} }
} }
@Composable @Composable
fun StartPartOfScreen(userPickerState: MutableStateFlow<AnimatedViewState>) { fun StartPartOfScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
if (chatModel.setDeliveryReceipts.value) { if (chatModel.setDeliveryReceipts.value) {
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
SetDeliveryReceiptsView(chatModel) SetDeliveryReceiptsView(chatModel)
}
} else { } else {
val stopped = chatModel.chatRunning.value == false val stopped = chatModel.chatRunning.value == false
if (chatModel.sharedContent.value == null) if (chatModel.sharedContent.value == null) {
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped) ChatListView(chatModel, userPickerState, AppLock::setPerformLA, stopped)
else }
} else {
// LALAL initial load of view doesn't show blur. Focusing text field shows it
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(keyboardCoversBar = false)) {
ShareListView(chatModel, stopped) ShareListView(chatModel, stopped)
} }
}
}
} }
@Composable @Composable
@@ -90,6 +90,9 @@ object ChatModel {
// Needed to check for bottom nav bar and to apply or not navigation bar color on Android // Needed to check for bottom nav bar and to apply or not navigation bar color on Android
val newChatSheetVisible = mutableStateOf(false) val newChatSheetVisible = mutableStateOf(false)
// Needed to apply black color to left/right cutout area on Android
val fullscreenGalleryVisible = mutableStateOf(false)
// preferences // preferences
val notificationPreviewMode by lazy { val notificationPreviewMode by lazy {
mutableStateOf( mutableStateOf(
@@ -1889,6 +1892,7 @@ class PendingContactConnection(
@Serializable @Serializable
enum class ConnStatus { enum class ConnStatus {
@SerialName("new") New, @SerialName("new") New,
@SerialName("prepared") Prepared,
@SerialName("joined") Joined, @SerialName("joined") Joined,
@SerialName("requested") Requested, @SerialName("requested") Requested,
@SerialName("accepted") Accepted, @SerialName("accepted") Accepted,
@@ -1898,6 +1902,7 @@ enum class ConnStatus {
val initiated: Boolean? get() = when (this) { val initiated: Boolean? get() = when (this) {
New -> true New -> true
Prepared -> false
Joined -> false Joined -> false
Requested -> true Requested -> true
Accepted -> true Accepted -> true
@@ -118,6 +118,9 @@ class AppPreferences {
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true) val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0) val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0)
// Blur broken on Android 12, see https://github.com/chrisbanes/haze/issues/77. And not available before 12
val deviceSupportsBlur = appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 32
val appearanceBarsBlurRadius = mkIntPreference(SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS, if (deviceSupportsBlur) 50 else 0)
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
@@ -223,6 +226,8 @@ class AppPreferences {
val chatItemTail = mkBoolPreference(SHARED_PREFS_CHAT_ITEM_TAIL, true) val chatItemTail = mkBoolPreference(SHARED_PREFS_CHAT_ITEM_TAIL, true)
val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f) val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f)
val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f) val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f)
val inAppBarsDefaultAlpha = if (deviceSupportsBlur) 0.875f else 0.975f
val inAppBarsAlpha = mkFloatPreference(SHARED_PREFS_IN_APP_BARS_ALPHA, inAppBarsDefaultAlpha)
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null) val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0) val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
@@ -244,7 +249,7 @@ class AppPreferences {
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true) val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false) val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, appPlatform.isAndroid) val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, true)
val hintPreferences: List<Pair<SharedPreference<Boolean>, Boolean>> = listOf( val hintPreferences: List<Pair<SharedPreference<Boolean>, Boolean>> = listOf(
laNoticeShown to false, laNoticeShown to false,
@@ -362,6 +367,7 @@ class AppPreferences {
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays" private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius" private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius"
private const val SHARED_PREFS_APPEARANCE_BARS_BLUR_RADIUS = "AppearanceBarsBlurRadius"
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
@@ -428,6 +434,7 @@ class AppPreferences {
private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail" private const val SHARED_PREFS_CHAT_ITEM_TAIL = "ChatItemTail"
private const val SHARED_PREFS_FONT_SCALE = "FontScale" private const val SHARED_PREFS_FONT_SCALE = "FontScale"
private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale" private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale"
private const val SHARED_PREFS_IN_APP_BARS_ALPHA = "InAppBarsAlpha"
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion" private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode" private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime" private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime"
@@ -1226,6 +1233,15 @@ object ChatController {
) )
return null return null
} }
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
&& r.chatError.agentError is AgentErrorType.SMP
&& r.chatError.agentError.smpErr is SMPErrorType.QUOTA -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.connection_error_quota),
generalGetString(MR.strings.connection_error_quota_desc)
)
return null
}
else -> { else -> {
if (!(networkErrorAlert(r))) { if (!(networkErrorAlert(r))) {
apiErrorAlert("apiConnect", generalGetString(MR.strings.connection_error), r) apiErrorAlert("apiConnect", generalGetString(MR.strings.connection_error), r)
@@ -6045,6 +6061,7 @@ sealed class SMPErrorType {
is AUTH -> "AUTH" is AUTH -> "AUTH"
is CRYPTO -> "CRYPTO" is CRYPTO -> "CRYPTO"
is QUOTA -> "QUOTA" is QUOTA -> "QUOTA"
is STORE -> "STORE ${storeErr}"
is NO_MSG -> "NO_MSG" is NO_MSG -> "NO_MSG"
is LARGE_MSG -> "LARGE_MSG" is LARGE_MSG -> "LARGE_MSG"
is EXPIRED -> "EXPIRED" is EXPIRED -> "EXPIRED"
@@ -6057,6 +6074,7 @@ sealed class SMPErrorType {
@Serializable @SerialName("AUTH") class AUTH: SMPErrorType() @Serializable @SerialName("AUTH") class AUTH: SMPErrorType()
@Serializable @SerialName("CRYPTO") class CRYPTO: SMPErrorType() @Serializable @SerialName("CRYPTO") class CRYPTO: SMPErrorType()
@Serializable @SerialName("QUOTA") class QUOTA: SMPErrorType() @Serializable @SerialName("QUOTA") class QUOTA: SMPErrorType()
@Serializable @SerialName("STORE") class STORE(val storeErr: String): SMPErrorType()
@Serializable @SerialName("NO_MSG") class NO_MSG: SMPErrorType() @Serializable @SerialName("NO_MSG") class NO_MSG: SMPErrorType()
@Serializable @SerialName("LARGE_MSG") class LARGE_MSG: SMPErrorType() @Serializable @SerialName("LARGE_MSG") class LARGE_MSG: SMPErrorType()
@Serializable @SerialName("EXPIRED") class EXPIRED: SMPErrorType() @Serializable @SerialName("EXPIRED") class EXPIRED: SMPErrorType()
@@ -14,20 +14,11 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import java.io.File import java.io.File
expect fun Modifier.navigationBarsWithImePadding(): Modifier
@Composable
expect fun ProvideWindowInsets(
consumeWindowInsets: Boolean = true,
windowInsetsAnimationsEnabled: Boolean = true,
content: @Composable () -> Unit
)
@Composable @Composable
expect fun Modifier.desktopOnExternalDrag( expect fun Modifier.desktopOnExternalDrag(
enabled: Boolean = true, enabled: Boolean = true,
onFiles: (List<File>) -> Unit = {}, onFiles: (List<File>) -> Unit = {},
onImage: (Painter) -> Unit = {}, onImage: (File) -> Unit = {},
onText: (String) -> Unit = {} onText: (String) -> Unit = {}
): Modifier ): Modifier
@@ -8,6 +8,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import chat.simplex.common.model.ChatId import chat.simplex.common.model.ChatId
import chat.simplex.common.model.NotificationsMode import chat.simplex.common.model.NotificationsMode
import chat.simplex.common.ui.theme.CurrentColors
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
interface PlatformInterface { interface PlatformInterface {
@@ -20,12 +21,12 @@ interface PlatformInterface {
fun androidChatInitializedAndStarted() {} fun androidChatInitializedAndStarted() {}
fun androidIsBackgroundCallAllowed(): Boolean = true fun androidIsBackgroundCallAllowed(): Boolean = true
fun androidSetNightModeIfSupported() {} fun androidSetNightModeIfSupported() {}
fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {} fun androidSetStatusAndNavigationBarAppearance(isLightStatusBar: Boolean, isLightNavBar: Boolean, blackNavBar: Boolean = false, themeBackgroundColor: Color = CurrentColors.value.colors.background) {}
fun androidSetDrawerStatusAndNavBarColor(isLight: Boolean, drawerShadingColor: Color, toolbarOnTop: Boolean, navBarColor: Color) {}
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {} fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
fun androidPictureInPictureAllowed(): Boolean = true fun androidPictureInPictureAllowed(): Boolean = true
fun androidCallEnded() {} fun androidCallEnded() {}
fun androidRestartNetworkObserver() {} fun androidRestartNetworkObserver() {}
val androidApiLevel: Int? get() = null
@Composable fun androidLockPortraitOrientation() {} @Composable fun androidLockPortraitOrientation() {}
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
@Composable fun desktopShowAppUpdateNotice() {} @Composable fun desktopShowAppUpdateNotice() {}
@@ -8,6 +8,7 @@ import androidx.compose.foundation.lazy.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
@@ -21,11 +22,44 @@ expect fun LazyColumnWithScrollBar(
horizontalAlignment: Alignment.Horizontal = Alignment.Start, horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true, userScrollEnabled: Boolean = true,
additionalBarOffset: State<Dp>? = null,
// by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here
// maxSize (at least maxHeight) is needed for blur on appBars to work correctly
fillMaxSize: Boolean = true,
content: LazyListScope.() -> Unit
)
@Composable
expect fun LazyColumnWithScrollBarNoAppBar(
modifier: Modifier = Modifier,
state: LazyListState? = null,
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
additionalBarOffset: State<Dp>? = null,
content: LazyListScope.() -> Unit content: LazyListScope.() -> Unit
) )
@Composable @Composable
expect fun ColumnWithScrollBar( expect fun ColumnWithScrollBar(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
state: ScrollState? = null,
// set true when you want to show something in the center with respected .fillMaxSize()
maxIntrinsicSize: Boolean = false,
// by default, this function will include .fillMaxSize() without you doing anything. If you don't need it, pass `false` here
// maxSize (at least maxHeight) is needed for blur on appBars to work correctly
fillMaxSize: Boolean = true,
content: @Composable ColumnScope.() -> Unit
)
@Composable
expect fun ColumnWithScrollBarNoAppBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top, verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start, horizontalAlignment: Alignment.Horizontal = Alignment.Start,
@@ -1,14 +1,14 @@
package chat.simplex.common.ui.theme package chat.simplex.common.ui.theme
import androidx.compose.foundation.background
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController import chat.simplex.common.model.ChatController
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
@@ -587,20 +587,26 @@ data class ThemeModeOverride (
} }
} }
fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier { fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, bgLayerSize: MutableState<IntSize>?, bgLayer: GraphicsLayer?/*, shape: Shape = RectangleShape*/): Modifier {
return if (baseTheme == DefaultTheme.SIMPLEX) { return drawBehind {
this.background(brush = Brush.linearGradient( copyBackgroundToAppBar(bgLayerSize, bgLayer) {
if (baseTheme == DefaultTheme.SIMPLEX) {
drawRect(brush = themedBackgroundBrush())
} else {
drawRect(CurrentColors.value.colors.background)
}
}
}
}
fun themedBackgroundBrush(): Brush = Brush.linearGradient(
listOf( listOf(
CurrentColors.value.colors.background.darker(0.4f), CurrentColors.value.colors.background.darker(0.4f),
CurrentColors.value.colors.background.lighter(0.4f) CurrentColors.value.colors.background.lighter(0.4f)
), ),
Offset(0f, Float.POSITIVE_INFINITY), Offset(0f, Float.POSITIVE_INFINITY),
Offset(Float.POSITIVE_INFINITY, 0f) Offset(Float.POSITIVE_INFINITY, 0f)
), shape = shape) )
} else {
this.background(color = CurrentColors.value.colors.background, shape = shape)
}
}
val DEFAULT_PADDING = 20.dp val DEFAULT_PADDING = 20.dp
val DEFAULT_SPACE_AFTER_ICON = 4.dp val DEFAULT_SPACE_AFTER_ICON = 4.dp
@@ -1,7 +1,6 @@
package chat.simplex.common.ui.theme package chat.simplex.common.ui.theme
import androidx.compose.material.Colors import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
@@ -107,7 +106,7 @@ object ThemeManager {
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
platform.androidSetNightModeIfSupported() platform.androidSetNightModeIfSupported()
val c = CurrentColors.value.colors val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !ChatController.appPrefs.oneHandUI.get(), ChatController.appPrefs.oneHandUI.get()) platform.androidSetStatusAndNavigationBarAppearance(c.isLight, c.isLight)
} }
fun changeDarkTheme(theme: String) { fun changeDarkTheme(theme: String) {
@@ -125,10 +124,6 @@ object ThemeManager {
themeIds[nonSystemThemeName] = prevValue.themeId themeIds[nonSystemThemeName] = prevValue.themeId
appPrefs.currentThemeIds.set(themeIds) appPrefs.currentThemeIds.set(themeIds)
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
if (name == ThemeColor.BACKGROUND) {
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false)
}
} }
fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) { fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) {
@@ -7,40 +7,34 @@ import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.* import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import kotlinx.coroutines.flow.collect import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID
import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout
import chat.simplex.common.views.chatlist.NavigationBarBackground
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun TerminalView(floating: Boolean = false, close: () -> Unit) { fun TerminalView(floating: Boolean = false) {
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) } val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
val close = {
close()
if (appPlatform.isDesktop) {
ModalManager.center.closeModals()
}
}
BackHandler(onBack = {
close()
})
TerminalLayout( TerminalLayout(
composeState, composeState,
floating, floating,
sendCommand = { sendCommand(chatModel, composeState) }, sendCommand = { sendCommand(chatModel, composeState) },
close
) )
} }
@@ -69,7 +63,6 @@ fun TerminalLayout(
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
floating: Boolean, floating: Boolean,
sendCommand: () -> Unit, sendCommand: () -> Unit,
close: () -> Unit
) { ) {
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) } val textStyle = remember { mutableStateOf(smallFont) }
@@ -77,12 +70,21 @@ fun TerminalLayout(
fun onMessageChange(s: String) { fun onMessageChange(s: String) {
composeState.value = composeState.value.copy(message = s) composeState.value = composeState.value.copy(message = s)
} }
val oneHandUI = remember { appPrefs.oneHandUI.state }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { Box(Modifier.fillMaxSize()) {
Scaffold( val composeViewHeight = remember { mutableStateOf(0.dp) }
topBar = { CloseSheetBar(close) }, AdaptingBottomPaddingLayout(Modifier, CONSOLE_COMPOSE_LAYOUT_ID, composeViewHeight) {
bottomBar = { TerminalLog(floating, composeViewHeight)
Column { Column(
Modifier
.layoutId(CONSOLE_COMPOSE_LAYOUT_ID)
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.consumeWindowInsets(PaddingValues(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp))
.imePadding()
.padding(bottom = if (oneHandUI.value) AppBarHeight * fontSizeSqrtMultiplier else 0.dp)
.background(MaterialTheme.colors.background)
) {
Divider() Divider()
Box(Modifier.padding(horizontal = 8.dp)) { Box(Modifier.padding(horizontal = 8.dp)) {
SendMsgView( SendMsgView(
@@ -110,32 +112,21 @@ fun TerminalLayout(
) )
} }
} }
},
contentColor = LocalContentColor.current,
modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding ->
Surface(
modifier = Modifier
.padding(contentPadding)
.fillMaxWidth(),
color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
) {
TerminalLog(floating)
} }
if (!oneHandUI.value) {
NavigationBarBackground(true, oneHandUI.value)
} }
} }
} }
@Composable @Composable
fun TerminalLog(floating: Boolean) { fun TerminalLog(floating: Boolean, composeViewHeight: State<Dp>) {
val reversedTerminalItems by remember { val reversedTerminalItems by remember {
derivedStateOf { chatModel.terminalItems.value.asReversed() } derivedStateOf { chatModel.terminalItems.value.asReversed() }
} }
val clipboard = LocalClipboardManager.current
val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState() val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
var autoScrollToBottom = true var autoScrollToBottom = listState.firstVisibleItemIndex <= 1
launch { launch {
snapshotFlow { listState.layoutInfo.totalItemsCount } snapshotFlow { listState.layoutInfo.totalItemsCount }
.filter { autoScrollToBottom } .filter { autoScrollToBottom }
@@ -150,12 +141,21 @@ fun TerminalLog(floating: Boolean) {
launch { launch {
snapshotFlow { listState.firstVisibleItemIndex } snapshotFlow { listState.firstVisibleItemIndex }
.collect { .collect {
autoScrollToBottom = listState.firstVisibleItemIndex == 0 autoScrollToBottom = it == 0
} }
} }
} }
LazyColumnWithScrollBar(reverseLayout = true, state = listState) { LazyColumnWithScrollBar (
reverseLayout = true,
contentPadding = PaddingValues(
top = topPaddingToContent(),
bottom = composeViewHeight.value
),
state = listState,
additionalBarOffset = composeViewHeight
) {
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item -> items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
val clipboard = LocalClipboardManager.current
val rhId = item.remoteHostId val rhId = item.remoteHostId
val rhIdStr = if (rhId == null) "" else "$rhId " val rhIdStr = if (rhId == null) "" else "$rhId "
Text( Text(
@@ -172,7 +172,8 @@ fun TerminalLog(floating: Boolean) {
ModalManager.start ModalManager.start
} }
modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) { modalPlace.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { ColumnWithScrollBar {
SelectionContainer {
val details = item.details val details = item.details
.let { .let {
if (it.length < 100_000) it if (it.length < 100_000) it
@@ -181,6 +182,7 @@ fun TerminalLog(floating: Boolean) {
Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) Text(details, modifier = Modifier.heightIn(max = 50_000.dp).padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
} }
} }
}
}.padding(horizontal = 8.dp, vertical = 4.dp) }.padding(horizontal = 8.dp, vertical = 4.dp)
) )
} }
@@ -208,8 +210,7 @@ fun PreviewTerminalLayout() {
TerminalLayout( TerminalLayout(
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) },
sendCommand = {}, sendCommand = {},
floating = false, floating = false
close = {}
) )
} }
} }
@@ -40,8 +40,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val keyboardState by getKeyboardState() val keyboardState by getKeyboardState()
var savedKeyboardState by remember { mutableStateOf(keyboardState) } var savedKeyboardState by remember { mutableStateOf(keyboardState) }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -50,11 +48,9 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
val displayName = rememberSaveable { mutableStateOf("") } val displayName = rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
ColumnWithScrollBar( ColumnWithScrollBar {
modifier = Modifier.fillMaxSize()
) {
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) AppBarTitle(stringResource(MR.strings.create_profile), withPadding = false, bottomPadding = DEFAULT_PADDING)
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text( Text(
stringResource(MR.strings.display_name), stringResource(MR.strings.display_name),
@@ -102,7 +98,6 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
} }
} }
} }
}
} }
@Composable @Composable
@@ -111,31 +106,15 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val keyboardState by getKeyboardState() val keyboardState by getKeyboardState()
var savedKeyboardState by remember { mutableStateOf(keyboardState) } var savedKeyboardState by remember { mutableStateOf(keyboardState) }
val handler = remember { AppBarHandler() } CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
CompositionLocalProvider( ModalView({
LocalAppBarHandler provides handler
) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Column(
modifier = Modifier
.fillMaxSize()
.themedBackground(),
horizontalAlignment = Alignment.CenterHorizontally
) {
CloseSheetBar(close = {
if (chatModel.users.none { !it.user.hidden }) { if (chatModel.users.none { !it.user.hidden }) {
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
} else { } else {
close() close()
} }
}) }) {
BackHandler(onBack = { ColumnWithScrollBar {
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
})
ColumnWithScrollBar(
modifier = Modifier.fillMaxSize()
) {
val displayName = rememberSaveable { mutableStateOf("") } val displayName = rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) { Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) {
@@ -165,7 +144,6 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
focusRequester.requestFocus() focusRequester.requestFocus()
} }
} }
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
setLastVersionDefault(chatModel) setLastVersionDefault(chatModel)
} }
@@ -255,7 +233,6 @@ fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isVal
val modifier = Modifier val modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = 50.dp) .heightIn(min = 50.dp)
.navigationBarsWithImePadding()
.onFocusChanged { focused = it.isFocused } .onFocusChanged { focused = it.isFocused }
Column( Column(
Modifier Modifier
@@ -289,6 +266,7 @@ fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isVal
enabled = true, enabled = true,
isError = false, isError = false,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
) )
} }
) )
@@ -53,7 +53,7 @@ fun IncomingCallAlertLayout(
acceptCall: () -> Unit acceptCall: () -> Unit
) { ) {
val color = if (isInDarkTheme()) MaterialTheme.colors.surface else IncomingCallLight val color = if (isInDarkTheme()) MaterialTheme.colors.surface else IncomingCallLight
Column(Modifier.fillMaxWidth().background(color).padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) { Column(Modifier.fillMaxWidth().background(color).statusBarsPadding().padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) {
IncomingCallInfo(invitation, chatModel) IncomingCallInfo(invitation, chatModel)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
@@ -529,10 +529,7 @@ fun ChatInfoLayout(
KeyChangeEffect(chat.id) { KeyChangeEffect(chat.id) {
scope.launch { scrollState.scrollTo(0) } scope.launch { scrollState.scrollTo(0) }
} }
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxWidth()
) {
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
@@ -276,7 +276,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
@Composable @Composable
fun HistoryTab() { fun HistoryTab() {
ColumnWithScrollBar(Modifier.fillMaxWidth()) { ColumnWithScrollBar {
Details() Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
val versions = ciInfo.itemVersions val versions = ciInfo.itemVersions
@@ -300,7 +300,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
@Composable @Composable
fun QuoteTab(qi: CIQuote) { fun QuoteTab(qi: CIQuote) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) { ColumnWithScrollBar {
Details() Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
@@ -313,7 +313,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
@Composable @Composable
fun ForwardedFromTab(forwardedFromItem: AChatItem) { fun ForwardedFromTab(forwardedFromItem: AChatItem) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) { ColumnWithScrollBar {
Details() Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
SectionView { SectionView {
@@ -375,7 +375,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
@Composable @Composable
fun DeliveryTab(memberDeliveryStatuses: List<MemberDeliveryStatus>) { fun DeliveryTab(memberDeliveryStatuses: List<MemberDeliveryStatus>) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) { ColumnWithScrollBar {
Details() Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
val mss = membersStatuses(chatModel, memberDeliveryStatuses) val mss = membersStatuses(chatModel, memberDeliveryStatuses)
@@ -12,10 +12,11 @@ import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.* import androidx.compose.ui.*
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.*
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.layout.layoutId import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@@ -108,6 +109,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
} }
} }
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false)) {
when (chatInfo) { when (chatInfo) {
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null } val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
@@ -523,28 +525,10 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
showSearch = showSearch showSearch = showSearch
) )
if (appPlatform.isAndroid) {
val backgroundColor = MaterialTheme.colors.background
val backgroundColorState = rememberUpdatedState(backgroundColor)
LaunchedEffect(Unit) {
snapshotFlow { ModalManager.center.modalCount.value > 0 }
.collect { modalBackground ->
if (modalBackground) {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
} else {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, false)
}
}
}
}
} }
} }
is ChatInfo.ContactConnection -> { is ChatInfo.ContactConnection -> {
val close = { chatModel.chatId.value = null } val close = { chatModel.chatId.value = null }
val handler = remember { AppBarHandler() }
CompositionLocalProvider(
LocalAppBarHandler provides handler
) {
ModalView(close, showClose = appPlatform.isAndroid, content = { ModalView(close, showClose = appPlatform.isAndroid, content = {
ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close) ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close)
}) })
@@ -554,13 +538,8 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
chatModel.chatItems.clear() chatModel.chatItems.clear()
} }
} }
}
is ChatInfo.InvalidJSON -> { is ChatInfo.InvalidJSON -> {
val close = { chatModel.chatId.value = null } val close = { chatModel.chatId.value = null }
val handler = remember { AppBarHandler() }
CompositionLocalProvider(
LocalAppBarHandler provides handler
) {
ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = { ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = {
InvalidJSONView(chatInfo.json) InvalidJSONView(chatInfo.json)
}) })
@@ -570,10 +549,10 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
chatModel.chatItems.clear() chatModel.chatItems.clear()
} }
} }
}
else -> {} else -> {}
} }
} }
}
} }
fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) { fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) {
@@ -642,24 +621,15 @@ fun ChatLayout(
.desktopOnExternalDrag( .desktopOnExternalDrag(
enabled = remember(attachmentDisabled.value, chatInfo.value?.userCanSend) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.userCanSend == true) }.value, enabled = remember(attachmentDisabled.value, chatInfo.value?.userCanSend) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.userCanSend == true) }.value,
onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) }, onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) },
onImage = { onImage = { file -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(file.toURI()), null) } },
// TODO: file is not saved anywhere?!
val tmpFile = File.createTempFile("image", ".bmp", tmpDir)
tmpFile.deleteOnExit()
chatModel.filesToDelete.add(tmpFile)
val uri = tmpFile.toURI()
CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(uri), null) }
},
onText = { onText = {
// Need to parse HTML in order to correctly display the content // Need to parse HTML in order to correctly display the content
//composeState.value = composeState.value.copy(message = composeState.value.message + it) //composeState.value = composeState.value.copy(message = composeState.value.message + it)
}, },
) )
) { ) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout( ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F), scrimColor = Color.Black.copy(alpha = 0.12F),
modifier = Modifier.navigationBarsWithImePadding(),
sheetElevation = 0.dp, sheetElevation = 0.dp,
sheetContent = { sheetContent = {
ChooseAttachmentView( ChooseAttachmentView(
@@ -670,53 +640,48 @@ fun ChatLayout(
sheetState = attachmentBottomSheetState, sheetState = attachmentBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) { ) {
val floatingButton: MutableState<@Composable () -> Unit> = remember { mutableStateOf({}) } val composeViewHeight = remember { mutableStateOf(0.dp) }
val setFloatingButton = { button: @Composable () -> Unit -> Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer)) {
floatingButton.value = button val remoteHostId = remember { remoteHostId }.value
val chatInfo = remember { chatInfo }.value
AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) {
if (chatInfo != null) {
Box(Modifier.fillMaxSize()) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
)
} }
}
Scaffold( val oneHandUI = remember { appPrefs.oneHandUI.state }
topBar = { Box(
Modifier
.layoutId(CHAT_COMPOSE_LAYOUT_ID)
.align(Alignment.BottomCenter)
.imePadding()
.navigationBarsPadding()
.then(if (oneHandUI.value) Modifier.padding(bottom = AppBarHeight * fontSizeSqrtMultiplier) else Modifier)
) {
composeView()
}
}
val oneHandUI = remember { appPrefs.oneHandUI.state }
if (oneHandUI.value) {
StatusBarBackground()
} else {
NavigationBarBackground(true, oneHandUI.value, noAlpha = true)
}
Box(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
if (selectedChatItems.value == null) { if (selectedChatItems.value == null) {
val chatInfo = chatInfo.value
if (chatInfo != null) { if (chatInfo != null) {
ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
} }
} else { } else {
SelectedItemsTopToolbar(selectedChatItems) SelectedItemsTopToolbar(selectedChatItems)
} }
},
bottomBar = composeView,
modifier = Modifier.navigationBarsWithImePadding(),
floatingActionButton = { floatingButton.value() },
contentColor = LocalContentColor.current,
backgroundColor = Color.Unspecified
) { contentPadding ->
val wallpaperImage = MaterialTheme.wallpaper.type.image
val wallpaperType = MaterialTheme.wallpaper.type
val backgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, MaterialTheme.colors.background)
val tintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base)
BoxWithConstraints(Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
.then(if (wallpaperImage != null)
Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) }
else
Modifier)
.padding(contentPadding)
) {
val remoteHostId = remember { remoteHostId }.value
val chatInfo = remember { chatInfo }.value
if (chatInfo != null) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
)
}
}
} }
} }
} }
@@ -724,7 +689,7 @@ fun ChatLayout(
} }
@Composable @Composable
fun ChatInfoToolbar( fun BoxScope.ChatInfoToolbar(
chatInfo: ChatInfo, chatInfo: ChatInfo,
back: () -> Unit, back: () -> Unit,
info: () -> Unit, info: () -> Unit,
@@ -876,23 +841,35 @@ fun ChatInfoToolbar(
} }
} }
} }
val oneHandUI = remember { appPrefs.oneHandUI.state }
DefaultTopAppBar( DefaultAppBar(
navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } }, navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } },
title = { ChatInfoToolbarTitle(chatInfo) }, title = { ChatInfoToolbarTitle(chatInfo) },
onTitleClick = if (chatInfo is ChatInfo.Local) null else info, onTitleClick = if (chatInfo is ChatInfo.Local) null else info,
showSearch = showSearch.value, showSearch = showSearch.value,
onTop = !oneHandUI.value,
onSearchValueChanged = onSearchValueChanged, onSearchValueChanged = onSearchValueChanged,
buttons = barButtons buttons = { barButtons.forEach { it() } }
) )
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier)) val density = LocalDensity.current
val width = remember { mutableStateOf(250.dp) }
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight * fontSizeSqrtMultiplier)) { val height = remember { mutableStateOf(0.dp) }
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(
showMenu,
modifier = Modifier.onSizeChanged { with(density) {
width.value = it.width.toDp().coerceAtLeast(250.dp)
if (oneHandUI.value && (appPlatform.isDesktop || (platform.androidApiLevel ?: 0) >= 30)) height.value = it.height.toDp()
} },
offset = DpOffset(-width.value, if (oneHandUI.value) -height.value else AppBarHeight)
) {
if (oneHandUI.value) {
menuItems.asReversed().forEach { it() }
} else {
menuItems.forEach { it() } menuItems.forEach { it() }
} }
} }
}
} }
@Composable @Composable
@@ -934,11 +911,12 @@ private fun ContactVerifiedShield() {
} }
@Composable @Composable
fun BoxWithConstraintsScope.ChatItemsList( fun BoxScope.ChatItemsList(
remoteHostId: Long?, remoteHostId: Long?,
chatInfo: ChatInfo, chatInfo: ChatInfo,
unreadCount: State<Int>, unreadCount: State<Int>,
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
composeViewHeight: State<Dp>,
searchValue: State<String>, searchValue: State<String>,
useLinkPreviews: Boolean, useLinkPreviews: Boolean,
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
@@ -963,7 +941,6 @@ fun BoxWithConstraintsScope.ChatItemsList(
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
setFloatingButton: (@Composable () -> Unit) -> Unit,
onComposed: suspend (chatId: String) -> Unit, onComposed: suspend (chatId: String) -> Unit,
developerTools: Boolean, developerTools: Boolean,
showViaProxy: Boolean showViaProxy: Boolean
@@ -987,14 +964,19 @@ fun BoxWithConstraintsScope.ChatItemsList(
PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages) PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(8.dp))
val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } } val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } }
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() } val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent().roundToPx() })
val scrollToItem: (Long) -> Unit = { itemId: Long -> val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
val index = reversedChatItems.indexOfFirst { it.id == itemId } val scrollToItem: State<(Long) -> Unit> = remember {
mutableStateOf(
{ itemId: Long ->
val index = reversedChatItems.value.indexOfFirst { it.id == itemId }
if (index != -1) { if (index != -1) {
scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) } scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) }
} }
} }
)
}
// TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once // TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once
LaunchedEffect(chatInfo.id) { LaunchedEffect(chatInfo.id) {
var stopListening = false var stopListening = false
@@ -1011,8 +993,18 @@ fun BoxWithConstraintsScope.ChatItemsList(
VideoPlayerHolder.releaseAll() VideoPlayerHolder.releaseAll()
} }
) )
LazyColumnWithScrollBar(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) { LazyColumnWithScrollBar(
itemsIndexed(reversedChatItems, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem -> Modifier.align(Alignment.BottomCenter),
state = listState,
reverseLayout = true,
contentPadding = PaddingValues(
top = topPaddingToContent(),
bottom = composeViewHeight.value
),
additionalBarOffset = composeViewHeight
) {
itemsIndexed(reversedChatItems.value, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem ->
val itemScope = rememberCoroutineScope()
CompositionLocalProvider( CompositionLocalProvider(
// Makes horizontal and vertical scrolling to coexist nicely. // Makes horizontal and vertical scrolling to coexist nicely.
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
@@ -1020,10 +1012,10 @@ fun BoxWithConstraintsScope.ChatItemsList(
) { ) {
val provider = { val provider = {
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed -> providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
scope.launch { itemScope.launch {
listState.scrollToItem( listState.scrollToItem(
kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1), kotlin.math.min(reversedChatItems.value.lastIndex, indexInReversed + 1),
-maxHeightRounded -maxHeight.value
) )
} }
} }
@@ -1036,7 +1028,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
tryOrShowError("${cItem.id}ChatItem", error = { tryOrShowError("${cItem.id}ChatItem", error = {
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
}) { }) {
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem.value, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
} }
} }
@@ -1044,7 +1036,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) { fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) {
val dismissState = rememberDismissState(initialValue = DismissValue.Default) { val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
if (it == DismissValue.DismissedToStart) { if (it == DismissValue.DismissedToStart) {
scope.launch { itemScope.launch {
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) { if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) {
if (composeState.value.editing) { if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
@@ -1241,16 +1233,17 @@ fun BoxWithConstraintsScope.ChatItemsList(
} }
val range = chatViewItemsRange(currIndex, prevHidden) val range = chatViewItemsRange(currIndex, prevHidden)
val reversed = reversedChatItems.value
if (revealed.value && range != null) { if (revealed.value && range != null) {
reversedChatItems.subList(range.first, range.last + 1).forEachIndexed { index, ci -> reversed.subList(range.first, range.last + 1).forEachIndexed { index, ci ->
val prev = if (index + range.first == prevHidden) prevItem else reversedChatItems[index + range.first + 1] val prev = if (index + range.first == prevHidden) prevItem else reversed[index + range.first + 1]
ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation) ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation)
} }
} else { } else {
ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation) ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation)
} }
if (i == reversedChatItems.lastIndex) { if (i == reversed.lastIndex) {
DateSeparator(cItem.meta.itemTs) DateSeparator(cItem.meta.itemTs)
} }
} }
@@ -1258,7 +1251,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) { if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) {
LaunchedEffect(cItem.id) { LaunchedEffect(cItem.id) {
scope.launch { itemScope.launch {
delay(600) delay(600)
markRead(CC.ItemRange(cItem.id, cItem.id), null) markRead(CC.ItemRange(cItem.id, cItem.id), null)
} }
@@ -1267,10 +1260,10 @@ fun BoxWithConstraintsScope.ChatItemsList(
} }
} }
} }
FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState) FloatingButtons(chatModel.chatItems, unreadCount, composeViewHeight, remoteHostId, chatInfo, searchValue, markRead, listState)
FloatingDate( FloatingDate(
Modifier.padding(top = 10.dp).align(Alignment.TopCenter), Modifier.padding(top = 10.dp + topPaddingToContent()).align(Alignment.TopCenter),
listState, listState,
) )
@@ -1325,87 +1318,65 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
} }
@Composable @Composable
fun BoxWithConstraintsScope.FloatingButtons( fun BoxScope.FloatingButtons(
chatItems: State<List<ChatItem>>, chatItems: State<List<ChatItem>>,
unreadCount: State<Int>, unreadCount: State<Int>,
composeViewHeight: State<Dp>,
remoteHostId: Long?, remoteHostId: Long?,
chatInfo: ChatInfo, chatInfo: ChatInfo,
searchValue: State<String>, searchValue: State<String>,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
setFloatingButton: (@Composable () -> Unit) -> Unit,
listState: LazyListState listState: LazyListState
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var firstVisibleIndex by remember { mutableStateOf(listState.firstVisibleItemIndex) } val maxHeight = remember { derivedStateOf { listState.layoutInfo.viewportSize.height } }
var lastIndexOfVisibleItems by remember { mutableStateOf(listState.layoutInfo.visibleItemsInfo.lastIndex) } val bottomUnreadCount = remember {
var firstItemIsVisible by remember { mutableStateOf(firstVisibleIndex == 0) }
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect {
firstVisibleIndex = it
firstItemIsVisible = firstVisibleIndex == 0
}
}
LaunchedEffect(listState) {
// When both snapshotFlows located in one LaunchedEffect second block will never be called because coroutine is paused on first block
// so separate them into two LaunchedEffects
snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex }
.distinctUntilChanged()
.collect {
lastIndexOfVisibleItems = it
}
}
val bottomUnreadCount by remember {
derivedStateOf { derivedStateOf {
if (unreadCount.value == 0) return@derivedStateOf 0 if (unreadCount.value == 0) return@derivedStateOf 0
val items = chatItems.value val items = chatItems.value
val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems val from = items.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex
if (items.size <= from || from < 0) return@derivedStateOf 0 if (items.size <= from || from < 0) return@derivedStateOf 0
items.subList(from, items.size).count { it.isRcvNew } items.subList(from, items.size).count { it.isRcvNew }
} }
} }
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
LaunchedEffect(bottomUnreadCount, firstItemIsVisible) { val showBottomButtonWithCounter = remember { derivedStateOf { bottomUnreadCount.value > 0 && listState.firstVisibleItemIndex != 0 && searchValue.value.isEmpty() } }
val showButtonWithCounter = bottomUnreadCount > 0 && !firstItemIsVisible && searchValue.value.isEmpty() val showBottomButtonWithArrow = remember { derivedStateOf { !showBottomButtonWithCounter.value && listState.firstVisibleItemIndex != 0 } }
val showButtonWithArrow = !showButtonWithCounter && !firstItemIsVisible BottomEndFloatingButton(
setFloatingButton(
bottomEndFloatingButton(
bottomUnreadCount, bottomUnreadCount,
showButtonWithCounter, showBottomButtonWithCounter,
showButtonWithArrow, showBottomButtonWithArrow,
composeViewHeight,
onClickArrowDown = { onClickArrowDown = {
scope.launch { listState.animateScrollToItem(0) } scope.launch { listState.animateScrollToItem(0) }
}, },
onClickCounter = { onClickCounter = {
scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount - 1), firstVisibleOffset) } val firstVisibleOffset = (-maxHeight.value * 0.8).toInt()
} scope.launch { listState.animateScrollToItem(kotlin.math.max(0, bottomUnreadCount.value - 1), firstVisibleOffset) }
))
} }
)
// Don't show top FAB if is in search // Don't show top FAB if is in search
if (searchValue.value.isNotEmpty()) return if (searchValue.value.isNotEmpty()) return
val fabSize = 56.dp val fabSize = 56.dp
val topUnreadCount by remember { val topUnreadCount = remember { derivedStateOf { unreadCount.value - bottomUnreadCount.value } }
derivedStateOf { unreadCount.value - bottomUnreadCount }
}
val showButtonWithCounter = topUnreadCount > 0
val height = with(LocalDensity.current) { maxHeight.toPx() }
val showDropDown = remember { mutableStateOf(false) } val showDropDown = remember { mutableStateOf(false) }
TopEndFloatingButton( TopEndFloatingButton(
Modifier.padding(end = DEFAULT_PADDING, top = 24.dp).align(Alignment.TopEnd), Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent()).align(Alignment.TopEnd),
topUnreadCount, topUnreadCount,
showButtonWithCounter, onClick = { scope.launch { listState.animateScrollBy(maxHeight.value.toFloat()) } },
onClick = { scope.launch { listState.animateScrollBy(height) } },
onLongClick = { showDropDown.value = true } onLongClick = { showDropDown.value = true }
) )
Box { Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd)) {
DefaultDropdownMenu(showDropDown, offset = DpOffset(this@FloatingButtons.maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) { val density = LocalDensity.current
val width = remember { mutableStateOf(250.dp) }
DefaultDropdownMenu(
showDropDown,
modifier = Modifier.onSizeChanged { with(density) { width.value = it.width.toDp().coerceAtLeast(250.dp) } },
offset = DpOffset(-DEFAULT_PADDING - width.value, 24.dp + fabSize + topPaddingToContent())
) {
ItemAction( ItemAction(
generalGetString(MR.strings.mark_read), generalGetString(MR.strings.mark_read),
painterResource(MR.images.ic_check), painterResource(MR.images.ic_check),
@@ -1413,7 +1384,7 @@ fun BoxWithConstraintsScope.FloatingButtons(
val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction
markRead( markRead(
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
bottomUnreadCount bottomUnreadCount.value
) )
showDropDown.value = false showDropDown.value = false
}) })
@@ -1475,12 +1446,11 @@ fun MemberImage(member: GroupMember) {
@Composable @Composable
private fun TopEndFloatingButton( private fun TopEndFloatingButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
unreadCount: Int, unreadCount: State<Int>,
showButtonWithCounter: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit onLongClick: () -> Unit
) = when { ) = when {
showButtonWithCounter -> { unreadCount.value > 0 -> {
val interactionSource = interactionSourceWithDetection(onClick, onLongClick) val interactionSource = interactionSourceWithDetection(onClick, onLongClick)
FloatingActionButton( FloatingActionButton(
{}, // no action here {}, // no action here
@@ -1490,7 +1460,7 @@ private fun TopEndFloatingButton(
interactionSource = interactionSource, interactionSource = interactionSource,
) { ) {
Text( Text(
unreadCountStr(unreadCount), unreadCountStr(unreadCount.value),
color = MaterialTheme.colors.primary, color = MaterialTheme.colors.primary,
fontSize = 14.sp, fontSize = 14.sp,
) )
@@ -1500,6 +1470,16 @@ private fun TopEndFloatingButton(
} }
} }
@Composable
fun topPaddingToContent(): Dp {
val oneHandUI = remember { appPrefs.oneHandUI.state }
return if (oneHandUI.value) {
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
} else {
AppBarHeight * fontSizeSqrtMultiplier + WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
}
}
@Composable @Composable
private fun FloatingDate( private fun FloatingDate(
modifier: Modifier, modifier: Modifier,
@@ -1509,8 +1489,9 @@ private fun FloatingDate(
var isNearBottom by remember { mutableStateOf(true) } var isNearBottom by remember { mutableStateOf(true) }
val lastVisibleItemDate = remember { val lastVisibleItemDate = remember {
derivedStateOf { derivedStateOf {
if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0 && listState.firstVisibleItemIndex >= 0) { if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0) {
val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex val lastFullyVisibleOffset = listState.layoutInfo.viewportEndOffset
val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - (listState.layoutInfo.visibleItemsInfo.lastOrNull { item -> item.offset + item.size <= lastFullyVisibleOffset && item.size > 0 }?.index ?: 0)
val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex) val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex)
val timeZone = TimeZone.currentSystemDefault() val timeZone = TimeZone.currentSystemDefault()
item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone) item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone)
@@ -1697,35 +1678,34 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (
} }
} }
private fun bottomEndFloatingButton( @Composable
unreadCount: Int, private fun BoxScope.BottomEndFloatingButton(
showButtonWithCounter: Boolean, unreadCount: State<Int>,
showButtonWithArrow: Boolean, showButtonWithCounter: State<Boolean>,
showButtonWithArrow: State<Boolean>,
composeViewHeight: State<Dp>,
onClickArrowDown: () -> Unit, onClickArrowDown: () -> Unit,
onClickCounter: () -> Unit onClickCounter: () -> Unit
): @Composable () -> Unit = when { ) = when {
showButtonWithCounter -> { showButtonWithCounter.value -> {
{
FloatingActionButton( FloatingActionButton(
onClick = onClickCounter, onClick = onClickCounter,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
modifier = Modifier.size(48.dp), modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
backgroundColor = MaterialTheme.colors.secondaryVariant, backgroundColor = MaterialTheme.colors.secondaryVariant,
) { ) {
Text( Text(
unreadCountStr(unreadCount), unreadCountStr(unreadCount.value),
color = MaterialTheme.colors.primary, color = MaterialTheme.colors.primary,
fontSize = 14.sp, fontSize = 14.sp,
) )
} }
} }
} showButtonWithArrow.value -> {
showButtonWithArrow -> {
{
FloatingActionButton( FloatingActionButton(
onClick = onClickArrowDown, onClick = onClickArrowDown,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
modifier = Modifier.size(48.dp), modifier = Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + composeViewHeight.value).align(Alignment.BottomEnd).size(48.dp),
backgroundColor = MaterialTheme.colors.secondaryVariant, backgroundColor = MaterialTheme.colors.secondaryVariant,
) { ) {
Icon( Icon(
@@ -1735,10 +1715,7 @@ private fun bottomEndFloatingButton(
) )
} }
} }
} else -> {}
else -> {
{}
}
} }
@Composable @Composable
@@ -1865,6 +1842,25 @@ private fun memberNames(member: GroupMember, prevMember: GroupMember?, memCount:
} }
} }
fun Modifier.chatViewBackgroundModifier(
colors: Colors,
wallpaper: AppWallpaper,
backgroundGraphicsLayerSize: MutableState<IntSize>?,
backgroundGraphicsLayer: GraphicsLayer?
): Modifier {
val wallpaperImage = wallpaper.type.image
val wallpaperType = wallpaper.type
val backgroundColor = wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, colors.background)
val tintColor = wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base)
return this
.then(if (wallpaperImage != null)
Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor, backgroundGraphicsLayerSize, backgroundGraphicsLayer) }
else
Modifier.drawWithCache { onDrawBehind { copyBackgroundToAppBar(backgroundGraphicsLayerSize, backgroundGraphicsLayer) { drawRect(backgroundColor) } } }
)
}
fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? = fun chatViewItemsRange(currIndex: Int?, prevHidden: Int?): IntRange? =
if (currIndex != null && prevHidden != null && prevHidden > currIndex) { if (currIndex != null && prevHidden != null && prevHidden > currIndex) {
currIndex..prevHidden currIndex..prevHidden
@@ -13,12 +13,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.filesToDelete import chat.simplex.common.model.ChatModel.filesToDelete
import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.ChatModel.withChats
@@ -896,7 +898,7 @@ fun ComposeView(
} }
} }
} }
Column(Modifier.background(MaterialTheme.colors.background)) { Box(Modifier.background(MaterialTheme.colors.background)) {
Divider() Divider()
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership) val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
@@ -918,7 +920,7 @@ fun ComposeView(
&& !nextSendGrpInv.value && !nextSendGrpInv.value
IconButton( IconButton(
attachmentClicked, attachmentClicked,
Modifier.padding(bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier),
enabled = attachmentEnabled enabled = attachmentEnabled
) { ) {
Icon( Icon(
@@ -81,10 +81,7 @@ private fun ContactPreferencesLayout(
reset: () -> Unit, reset: () -> Unit,
savePrefs: () -> Unit, savePrefs: () -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.contact_preferences)) AppBarTitle(stringResource(MR.strings.contact_preferences))
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) } val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
val onTTLUpdated = { ttl: Int? -> val onTTLUpdated = { ttl: Int? ->
@@ -1,9 +1,11 @@
package chat.simplex.common.views.chat package chat.simplex.common.views.chat
import SectionBottomSpacer
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCodeScanner import chat.simplex.common.views.newchat.QRCodeScanner
@@ -12,9 +14,7 @@ import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
Column( ColumnWithScrollBar {
Modifier.fillMaxSize()
) {
AppBarTitle(stringResource(MR.strings.scan_code)) AppBarTitle(stringResource(MR.strings.scan_code))
QRCodeScanner { text -> QRCodeScanner { text ->
verifyCode(text) { verifyCode(text) {
@@ -28,5 +28,6 @@ fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: ()
} }
} }
Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING)) Text(stringResource(MR.strings.scan_code_from_contacts_app), Modifier.padding(horizontal = DEFAULT_PADDING))
SectionBottomSpacer()
} }
} }
@@ -12,6 +12,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.BackHandler import chat.simplex.common.platform.BackHandler
import chat.simplex.common.platform.chatModel import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@@ -20,11 +21,12 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
@Composable @Composable
fun SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) { fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
val onBackClicked = { selectedChatItems.value = null } val onBackClicked = { selectedChatItems.value = null }
BackHandler(onBack = onBackClicked) BackHandler(onBack = onBackClicked)
val count = selectedChatItems.value?.size ?: 0 val count = selectedChatItems.value?.size ?: 0
DefaultTopAppBar( val oneHandUI = remember { appPrefs.oneHandUI.state }
DefaultAppBar(
navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) }, navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) },
title = { title = {
Text( Text(
@@ -39,10 +41,9 @@ fun SelectedItemsTopToolbar(selectedChatItems: MutableState<Set<Long>?>) {
) )
}, },
onTitleClick = null, onTitleClick = null,
showSearch = false, onTop = !oneHandUI.value,
onSearchValueChanged = {}, onSearchValueChanged = {},
) )
Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier))
} }
@Composable @Composable
@@ -68,6 +69,8 @@ fun SelectedItemsBottomToolbar(
Modifier Modifier
.matchParentSize() .matchParentSize()
.background(MaterialTheme.colors.background) .background(MaterialTheme.colors.background)
.padding(horizontal = 2.dp)
.height(AppBarHeight * fontSizeSqrtMultiplier)
.pointerInput(Unit) { .pointerInput(Unit) {
detectGesture { detectGesture {
true true
@@ -103,6 +106,7 @@ fun SelectedItemsBottomToolbar(
) )
} }
} }
Divider(Modifier.align(Alignment.TopStart))
} }
LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) { LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) {
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited) recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
@@ -7,7 +7,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.* import androidx.compose.foundation.shape.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.* import androidx.compose.ui.*
@@ -15,6 +14,7 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
@@ -61,7 +61,8 @@ fun SendMsgView(
) { ) {
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) } val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
Box(Modifier.padding(vertical = if (appPlatform.isAndroid) 8.dp else 6.dp)) { val padding = if (appPlatform.isAndroid) PaddingValues(vertical = 8.dp) else PaddingValues(top = 3.dp, bottom = 4.dp)
Box(Modifier.padding(padding)) {
val cs = composeState.value val cs = composeState.value
var progressByTimeout by rememberSaveable { mutableStateOf(false) } var progressByTimeout by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(composeState.value.inProgress) { LaunchedEffect(composeState.value.inProgress) {
@@ -147,7 +148,7 @@ fun SendMsgView(
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value) && (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
&& cs.contextItem is ComposeContextItem.NoContextItem && cs.contextItem is ComposeContextItem.NoContextItem
) { ) {
Spacer(Modifier.width(10.dp)) Spacer(Modifier.width(12.dp))
StartLiveMessageButton(userCanSend) { StartLiveMessageButton(userCanSend) {
if (composeState.value.preview is ComposePreview.NoPreview) { if (composeState.value.preview is ComposePreview.NoPreview) {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown) startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
@@ -423,6 +424,7 @@ private fun SendMsgButton(
onLongClick: (() -> Unit)? = null onLongClick: (() -> Unit)? = null
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val ripple = remember { ripple(bounded = false, radius = 24.dp) }
Box( Box(
modifier = Modifier.requiredSize(36.dp) modifier = Modifier.requiredSize(36.dp)
.combinedClickable( .combinedClickable(
@@ -431,7 +433,7 @@ private fun SendMsgButton(
enabled = enabled, enabled = enabled,
role = Role.Button, role = Role.Button,
interactionSource = interactionSource, interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 24.dp) indication = ripple
) )
.onRightClick { onLongClick?.invoke() }, .onRightClick { onLongClick?.invoke() },
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@@ -454,6 +456,7 @@ private fun SendMsgButton(
@Composable @Composable
private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) { private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val ripple = remember { ripple(bounded = false, radius = 24.dp) }
Box( Box(
modifier = Modifier.requiredSize(36.dp) modifier = Modifier.requiredSize(36.dp)
.clickable( .clickable(
@@ -461,7 +464,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
enabled = enabled, enabled = enabled,
role = Role.Button, role = Role.Button,
interactionSource = interactionSource, interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 24.dp) indication = ripple
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@@ -56,11 +56,7 @@ private fun VerifyCodeLayout(
connectionVerified: Boolean, connectionVerified: Boolean,
verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) {
Modifier
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(MR.strings.security_code), withPadding = false) AppBarTitle(stringResource(MR.strings.security_code), withPadding = false)
val splitCode = splitToParts(connectionCode, 24) val splitCode = splitToParts(connectionCode, 24)
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) { Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
@@ -130,10 +130,7 @@ fun AddGroupMembersLayout(
} }
} }
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.button_add_members)) AppBarTitle(stringResource(MR.strings.button_add_members))
profileText() profileText()
Spacer(Modifier.size(DEFAULT_PADDING)) Spacer(Modifier.size(DEFAULT_PADDING))
@@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@@ -283,9 +284,14 @@ fun ModalData.GroupChatInfoLayout(
if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) } if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) }
} }
} }
Box {
val oneHandUI = remember { appPrefs.oneHandUI.state }
LazyColumnWithScrollBar( LazyColumnWithScrollBar(
Modifier contentPadding = if (oneHandUI.value) {
.fillMaxWidth(), PaddingValues(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp, bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding())
} else {
PaddingValues(top = topPaddingToContent())
},
state = listState state = listState
) { ) {
item { item {
@@ -397,6 +403,11 @@ fun ModalData.GroupChatInfoLayout(
} }
} }
SectionBottomSpacer() SectionBottomSpacer()
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
}
}
if (!oneHandUI.value) {
NavigationBarBackground(oneHandUI.value, oneHandUI.value)
} }
} }
} }
@@ -119,9 +119,7 @@ fun GroupLinkLayout(
) )
} }
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier,
) {
AppBarTitle(stringResource(MR.strings.group_link)) AppBarTitle(stringResource(MR.strings.group_link))
Text( Text(
stringResource(MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect), stringResource(MR.strings.you_can_share_group_link_anybody_will_be_able_to_connect),
@@ -313,10 +313,7 @@ fun GroupMemberInfoLayout(
} }
} }
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
.fillMaxWidth(),
) {
Row( Row(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
@@ -82,9 +82,7 @@ private fun GroupPreferencesLayout(
reset: () -> Unit, reset: () -> Unit,
savePrefs: () -> Unit, savePrefs: () -> Unit,
) { ) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.group_preferences)) AppBarTitle(stringResource(MR.strings.group_preferences))
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) } val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
val onTTLUpdated = { ttl: Int? -> val onTTLUpdated = { ttl: Int? ->
@@ -82,10 +82,9 @@ fun GroupProfileLayout(
}, close) }, close)
} }
} }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout( ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F), scrimColor = Color.Black.copy(alpha = 0.12F),
modifier = Modifier.navigationBarsWithImePadding(), modifier = Modifier.imePadding(),
sheetContent = { sheetContent = {
GetImageBottomSheet( GetImageBottomSheet(
chosenImage, chosenImage,
@@ -98,9 +97,7 @@ fun GroupProfileLayout(
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) { ) {
ModalView(close = closeWithAlert) { ModalView(close = closeWithAlert) {
ColumnWithScrollBar( ColumnWithScrollBar {
Modifier
) {
Column( Column(
Modifier.fillMaxWidth() Modifier.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING) .padding(horizontal = DEFAULT_PADDING)
@@ -177,7 +174,6 @@ fun GroupProfileLayout(
} }
} }
} }
}
} }
private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean = private fun canUpdateProfile(displayName: String, groupProfile: GroupProfile): Boolean =

Some files were not shown because too many files have changed in this diff Show More