Compare commits

..

250 Commits

Author SHA1 Message Date
Avently 16edc24c16 android, desktop: testing grouping of chat items 2024-10-14 19:50:41 +07:00
Evgeny Poberezkin 1cb3c25478 docs: correction 2024-10-13 22:01:14 +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
Evgeny Poberezkin ec014d721e sdk: fix test 2024-10-10 19:15:09 +01:00
Yaroslav Pavlov 75bacb7923 desktop: fix typescript sdk ability to send / receive messages (#4970)
* typescript sdk: fix send messages

* typescript sdk: fix send messages naming
2024-10-10 19:10:11 +01:00
Stanislav Dmitrenko 0d8c179861 ios: fix not showing link creation and add group members pages (#5020) 2024-10-10 19:08:03 +01:00
Diogo e9a99dfb3c ios: fix empty qr code reader when swapping to connect via link (#5016) 2024-10-10 19:06:25 +01:00
Diogo baa585357f multiplatform: disable chat buttons on user picker when chat is stopped (#5017)
* ios: disable chat buttons on user picker when chat is stopped

* small change

* disable use from desktop on android when chat stopped
2024-10-10 19:01:31 +01:00
Diogo cebb4aa93b ios: fix ocassional error on getSubsTotal (#5021) 2024-10-10 18:55:37 +01:00
Stanislav Dmitrenko df53ae9d4f ios: fix remaining bugs in calls (#5013) 2024-10-10 12:11:01 +01:00
Evgeny 21b1904b0e ui: translations (#5015)
* Translated using Weblate (Italian)

Currently translated at 100.0% (2079 of 2079 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1831 of 1831 strings)

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

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

Currently translated at 100.0% (2079 of 2079 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2079 of 2079 strings)

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

* Translated using Weblate (Polish)

Currently translated at 99.2% (2064 of 2079 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (1831 of 1831 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 100.0% (2079 of 2079 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Polish)

Currently translated at 99.7% (2083 of 2089 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 99.5% (2079 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% (1831 of 1831 strings)

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

* 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% (1831 of 1831 strings)

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

* Translated using Weblate (Polish)

Currently translated at 99.9% (2088 of 2089 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1831 of 1831 strings)

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

* Translated using Weblate (Turkish)

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/tr/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1831 of 1831 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 43.0% (899 of 2089 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 12.5% (263 of 2089 strings)

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

* 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 (Vietnamese)

Currently translated at 43.1% (901 of 2089 strings)

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

* 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 100.0% (2089 of 2089 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1831 of 1831 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2089 of 2089 strings)

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

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

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/zh_Hans/

* 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% (1831 of 1831 strings)

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

* Translated using Weblate (Arabic)

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/ar/

* Translated using Weblate (Greek)

Currently translated at 11.6% (243 of 2089 strings)

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

* Translated using Weblate (Greek)

Currently translated at 1.2% (22 of 1831 strings)

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

* 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% (1831 of 1831 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 99.9% (1830 of 1831 strings)

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

* 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% (1831 of 1831 strings)

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

* process localizations

---------

Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: Abdullah Koyuncu <wisewebworks@outlook.com>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com>
Co-authored-by: billy appetie <billy_appetie@users.noreply.hosted.weblate.org>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: diodepon <diopon@mailo.com>
Co-authored-by: J R <jr@simplex.chat>
2024-10-10 11:30:51 +01:00
Diogo b1ef442f1e android, desktop: fix profile switching failure handling (#5014) 2024-10-10 14:02:05 +04:00
Evgeny Poberezkin 4020cb074f ios: export localizations 2024-10-10 11:01:15 +01:00
Arturs Krumins 0c69f6553a ios: fix group member sheet load animation (#5008)
* ios: fix group member sheet load animation

* improve diff

* rename

* revert moving sections

* re-indent

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-10 10:57:15 +01:00
Evgeny 03865b4a18 ios: v6.1 whats new (#5005)
* ios: v6.1 whats new

* update

* android, desktop
2024-10-09 15:56:05 +01:00
Evgeny ac5f0bc7bb ui: allow deleting and moderating up to 200 messages (#5010) 2024-10-09 12:31:51 +01:00
spaced4ndy 12423f4afa core: test db indexes (#4999) 2024-10-09 15:15:58 +04:00
Stanislav Dmitrenko fabbe0285d ui: rely on different value in stats when checking calls media (#5007)
* ui: rely on different value in stats when checking calls media

* int64
2024-10-09 08:37:21 +01:00
Evgeny Poberezkin b01efd9d0a Merge branch 'stable' 2024-10-08 20:55:46 +01:00
Evgeny Poberezkin fc28ff0f15 6.1-beta.4: ios 242, android 245, desktop 71 2024-10-08 19:08:54 +01:00
Evgeny 213f6cf372 website: translations (#5003)
* Translated using Weblate (Hungarian)

Currently translated at 100.0% (257 of 257 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (257 of 257 strings)

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

---------

Co-authored-by: summoner001 <summoner@vivaldi.net>
2024-10-08 18:30:50 +01:00
Evgeny a764e1906d ui: translations (#5002)
* Translated using Weblate (Dutch)

Currently translated at 99.9% (2071 of 2072 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 99.9% (1822 of 1823 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (1823 of 1823 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2072 of 2072 strings)

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

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

Currently translated at 100.0% (2072 of 2072 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2072 of 2072 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2072 of 2072 strings)

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

* Translated using Weblate (Portuguese)

Currently translated at 46.2% (958 of 2072 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.8% (2069 of 2072 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% (2072 of 2072 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Turkish)

Currently translated at 95.0% (1974 of 2076 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2076 of 2076 strings)

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

* Translated using Weblate (Turkish)

Currently translated at 95.9% (1991 of 2076 strings)

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

* Translated using Weblate (Hungarian)

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

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

* process localizations

* restore "Chat profile" translations

---------

Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Antonio Oliveira <antoniovini47@gmail.com>
Co-authored-by: Eraorahan <eraorahan@gmail.com>
Co-authored-by: Abdullah Koyuncu <wisewebworks@outlook.com>
2024-10-08 18:27:23 +01:00
Stanislav Dmitrenko 54b40a5838 android, desktop: checking for camera in calls and handle uninitialized call (#4997)
* android, desktop: checking for camera in calls and handle uninitialized call

* explanation for situation without permission

* reorder

* reorder

* strings

* font

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-08 17:16:34 +01:00
Stanislav Dmitrenko 35fc0544a0 ui: do not enable speaker in calls when contact turned on video (#4998) 2024-10-08 15:57:33 +01:00
Evgeny 6907f02ea6 android, desktop: additional options for transport isolation mode (#4994)
* android, desktop: additional options for transport isolation mode

* small changes

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-10-08 14:36:08 +01:00
spaced4ndy c6ece8afdd ios: update library 2024-10-08 17:13:17 +04:00
spaced4ndy a00e5901de core: 6.1.0.7 2024-10-08 16:42:36 +04:00
spaced4ndy ad2a0024f2 core: add missing indexes (#4993) 2024-10-08 15:26:16 +04:00
Stanislav Dmitrenko c5261a416f ios: calls switching from audio to video and back (#4964)
* ios: switch calls

* working audio/video calls without screen recording

* ui

* follow up

* audio devices & permissions

* padding

* backward compatibility

* cleanup & fix

* buttons foreground color and converting call to video call from CallKit

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-08 08:49:13 +01:00
Stanislav Dmitrenko 193e17f7af android, desktop: thread-safe terminal items and floating terminal improvements (#4992)
* android, desktop: thread-safe terminal items and floating terminal improvements

* optimization
2024-10-08 08:08:22 +01:00
Evgeny Poberezkin 87fd642951 core: 6.1.0.6 2024-10-07 23:31:52 +01:00
Evgeny fb044000d2 cli: option to use web port 443 with SMP servers when port is not specified (#4942)
* cli: option to use web port 443 with SMP servers when port is not specified

* ui types

* remove imports
2024-10-07 23:30:52 +01:00
Arturs Krumins 69791dbdcf ios: fix appearance settings scroll hang (#4967)
* ios: fix appearance settings scroll hang

* simplify

* Revert "simplify"

This reverts commit f7b0aa74a4.

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-07 20:29:46 +01:00
Arturs Krumins dc11202250 ios: fix animator crash (#4990) 2024-10-07 20:22:06 +01:00
Evgeny 96ac0a7715 core: update simplexmq (transport block encryption) (#4881) 2024-10-07 18:39:31 +01:00
Arturs Krumins bdb6bd6e20 ios: hide user picker sheet instantly, when opening another sheet (#4927)
* ios: hide user picker sheet instantly, when opening another sheet

* tweak appearance

* distance based animation duration

* cleanup; dismiss

* implement UIViewPropertyAnimator

* resolve warning

* user picker bottom padding

* reset user scroll position on dismiss; cleanup

* reduce dif

* delay user picker closing

* touchable list row; prevent tap gesture passtrough

* fix dark mode tap target; retain highlight; highlight in user scroller

* fix layout loop; add upper animation speed constraint

* refactor separators

* instantanious longPress; tweak animations

* cubic animation curve; dynamic backdrop opacity

* remove touchdown animation

* ios: user picker sheet concurent animation (#4955)

* ios: user picker sheet concurent animation

* bind showSettings; cleanup

* async qr code generation

* fix iOS15 sheet animation when presenting sheet multiple times

* async camera authorization in 'Use from desktop' sheet

* load sheet navigation titles before presenting (#4963)

* load sheet navigation titles before presenting

* list background during loading

* improve handling of repeated sheet presentation state changes

* fix keyboard related glitches

* ios: remove `showSettings` and `withNavigation` (#4980)

* remove showSettings

* pass dismiss action trough navigation links

* move auth to all sheets

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-07 18:30:17 +01:00
spaced4ndy 7ccd80bf23 core, ios: try to get next ntf message to get expected (#4962) 2024-10-07 19:35:38 +04:00
Arturs Krumins f0d6f15393 ios: prevent image encoding from blocking the UI (#4966)
* ios: prevent image encoding from blocking the UI

* let

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-05 20:11:57 +01:00
Evgeny 49c91283d8 Translated using Weblate (Portuguese (Brazil)) (#4959)
Currently translated at 99.2% (255 of 257 strings)

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

Co-authored-by: Antonio Oliveira <antoniovini47@gmail.com>
2024-10-05 19:56:24 +01:00
Evgeny 55de5b9ce9 ui: translations (#4956)
* Translated using Weblate (German)

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1824 of 1824 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (2067 of 2067 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 97.5% (1780 of 1824 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Romanian)

Currently translated at 35.0% (724 of 2067 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 40.4% (837 of 2067 strings)

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

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

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 89.5% (1852 of 2067 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 41.4% (856 of 2067 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1824 of 1824 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2067 of 2067 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 99.9% (1823 of 1824 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 42.6% (881 of 2067 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1824 of 1824 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 49.5% (903 of 1824 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2068 of 2068 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (2068 of 2068 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (2068 of 2068 strings)

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

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

Currently translated at 100.0% (2068 of 2068 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2068 of 2068 strings)

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

* Translated using Weblate (Polish)

Currently translated at 98.9% (2046 of 2068 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2068 of 2068 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2068 of 2068 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Turkish)

Currently translated at 95.2% (1970 of 2068 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2069 of 2069 strings)

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

* Translated using Weblate (Hungarian)

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

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

* process localizations

---------

Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: Zorro <3zorro.1@gmail.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Miyu Sakatsuki <miyu-sakatsuki@outlook.jp>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: Sarah Camila Lima <sarahnxjlima@gmail.com>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: c0d3r3d99 <chiara.ru2012@gmail.com>
Co-authored-by: Tadeusz Magura-Witkowski <tadeuszmw@gmail.com>
Co-authored-by: Batuhan Aydoğan <batuhanaydogan06@hotmail.com>
2024-10-05 19:55:45 +01:00
Diogo 8727d3b91b core: add chat message tail and roundness settings (#4977)
* core: add chat message tail and roundness settings

* ios: import/export chat message tail and roundness settings (#4978)
2024-10-05 19:44:26 +01:00
Diogo bb2a6ec65d android, desktop: add chat message tail and roundness settings (#4958)
* android, desktop: add roundness setting to chat items

* add tail setting

* use shape for clip

* wip tails

* shape style

* show tail only on last msg in group

* roundings

* padding for direct chats

* groups padding

* space between messages in settings preview

* refactor group paddings

* simplify

* simplify

* RcvDeleted handling

* revert uncessary

* import

* always maintain tail position

* rename

* reactions should not move

* short emoji shouldn't have tail

* remove invisible tail for voice without text

* better usage of gutters

* simplify

* rename

* simplify reactions

* linter happy

* exclude moderated items from shape

* uncessary diff

* func position

* fix chat view align on font resize (with image)

* fix tails moving bubble on max width

* fix big group names sometimes changing position

* small refactor

* fix top left corner end position

* rename

* sticky steps

* revert whitespace changes

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-05 19:02:09 +01:00
Evgeny Poberezkin 6118fca000 readme: update group links 2024-10-01 08:45:13 +01:00
Evgeny Poberezkin 026a8022e0 6.1-beta.3: ios 241, android 244, desktop 70 2024-09-30 23:00:16 +01:00
Evgeny Poberezkin dc1106afad ios: update core library 2024-09-30 19:56:09 +01:00
Evgeny Poberezkin cc9b4f3bb3 core: 6.1.0.5 (simplexmq: 6.1.0.3) 2024-09-30 18:29:20 +01:00
Diogo 533d0e40ac android, desktop: add floating date separator to chatview (#4951)
* android, desktop: add floating date separator to chatview

* closer near bottom

* uncessary code

* same pill bg as other btns

* space

* varname

* safe get for lastVisibleItem

* move floating date outside of floating buttons

* fast cleanup on chat change

* reduced recomposes

* change delay position

* base near bottom offset on viewport size

* refactor

* Revert "change delay position"

This reverts commit 27b19580ed.

* simplified

* exact match on header position

* reduce recomposes

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-30 15:45:32 +01:00
Stanislav Dmitrenko d9ad755474 android, desktop: make audio call default type of call on desktop (#4954)
* android, desktop: make audio call default type of call on desktop

* change
2024-09-30 15:45:13 +01:00
Stanislav Dmitrenko 15ca662805 android, desktop: support old Android WebViews (up to 69) (#4953)
* android, desktop: support old Android WebViews (up to 69)

* refactor

* WebView 70

* comment
2024-09-30 15:44:35 +01:00
spaced4ndy ab034e626f core: update simplexmq (#4952) 2024-09-30 14:53:36 +04:00
Diogo fc83bc692a android, desktop: make space on chat bubble end consistent (#4946)
* android, desktop: make space on chat bubble end consistent

* use non breaking spaces for reserve space

* avoid first white space non breaking to not drag content down
2024-09-28 18:26:43 +01:00
Evgeny Poberezkin d20d444e6e readme: update group links 2024-09-27 22:29:23 +01:00
Stanislav Dmitrenko fc0879ebb7 android, desktop: fix Safari sound (#4947)
* android, desktop: fix Safari sound

* another approach

* test

* Revert "test"

This reverts commit f89a30a88e.

* Revert "another approach"

This reverts commit 824ab7047c.

* Revert "android, desktop: fix Safari sound"

This reverts commit 80a866d472.

* android, desktop: fix Safari sound

* dependencies
2024-09-27 22:04:16 +01:00
Evgeny Poberezkin f048ddb922 6.1-beta.2: ios 240, android 243, desktop 69 2024-09-26 21:56:04 +01:00
Evgeny Poberezkin 0e39a62ab1 ios: update core library 2024-09-26 21:03:41 +01:00
Diogo 53f0fe9ca4 android, desktop: time based message grouping and day separators (#4914)
* android, desktop: message grouping

* short format on chat

* separator for dates

* simplify

* show on separator when not current year

* default for showing date on markdown text

* remove unused code

* refactor

* refactor

* remove default locally

* fixed build

* fix

* show first date in chat

* apply padding to selectable area

* fix date on chats for previous days

* add year formatting

* fixed message grouping and time show

* remove log

* fixed reserved space for meta

* align first chat bubble with image

* metadata correct space

* remove log

* simplify item separation logic

* cleanuo

* icon tweaks

* without unneeded element

* match ios logic

* CIMetaText fix

* split selectable area

* Revert "split selectable area"

This reverts commit 1c6001ba3d.

* reserve space similar to ios

* split spacing for chat item selection

* less repeated code

* format

* increase padding

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-26 20:26:33 +01:00
Stanislav Dmitrenko 95c1d8d798 android, desktop: calls switching from audio to video and back (#4814)
* android, desktop: calls switching from audio to video and back

* refactor

* working all 4 streams with mute handling differently

* changes

* changes

* wrong file

* changes

* padding

* android camera service type

* icons, sizes, clickable

* refactor

* Revert "android camera service type"

This reverts commit 9878ff38e9.

* late init camera permissions

* enabling camera sooner than call establishes (not fully done)

* changes

* alpha

* fixes for Safari

* enhancements

* fix Safari sound

* padding between buttons on desktop

* android default values for padding

* changes

* calls without encryption are supported and flipping camera on some devices works

* unused param

* logs

* background color

* play local video in Safari

* no line height

* removed one listener from per frame processing

* enhancements

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-26 20:18:05 +01:00
Evgeny 4a39b481b1 ios: avoid message changing width when sent/received ticks appear (#4945) 2024-09-26 17:28:14 +01:00
Evgeny Poberezkin 65c7ecbddf core: 6.1.0.4 (simplexmq 6.1.0.1) 2024-09-26 13:45:12 +01:00
Diogo 67472b6285 android, desktop: scrolling user profiles (#4939)
* android, desktop: scrolling user profiles

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-26 09:00:10 +01:00
Arturs Krumins 9199fbffd5 ios: fix add members search keyboard focus (#4934)
* ios: fix add members search keyboard focus

* use -1 as ID

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-25 22:26:04 +01:00
spaced4ndy 6e5eb697a2 core: use broker ts for member profile update item ts (#4940) 2024-09-25 18:33:20 +01:00
spaced4ndy e2e6935e5b core: fix reactions not being read on item updates (#4938) 2024-09-25 11:16:32 +01:00
Evgeny Poberezkin 769ef25c31 Merge branch 'stable' 2024-09-24 23:01:20 +01:00
Evgeny Poberezkin 2f730d54e9 6.0.5: ios 239, android 241, desktop 68 2024-09-24 21:48:30 +01:00
Evgeny Poberezkin fe0013c4a9 ios: update core library 2024-09-24 17:51:34 +01:00
Stanislav Dmitrenko 5261886b31 android, desktop: proxy configuration includes credentials (#4892)
* android, desktop: proxy configuration includes credentials

* migration

* changes for disabled socks

* migration

* port

* new logic

* migration

* check validity of fields

* validity of host

* import changes proxy just in case

* send port always

* non-nullable

* Revert "send port always"

This reverts commit 14dd066d80.

* string

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-24 17:07:39 +01:00
Evgeny 93ab3076d4 ios: SOCKS proxy UI (#4893)
* ios: SOCKS proxy UI

* update network config

* proxy

* adapt

* move, dont default to localhost:9050

* move socks proxy to defaults

* sock proxy preference

* rename

* rename

* fix

* fix

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-24 17:06:00 +01:00
Stanislav Dmitrenko 3b88ddbd4f desktop: fix vlc dependency (2) (#4869) 2024-09-24 17:04:08 +01:00
spaced4ndy 54ff95f350 ios: fix theme customization changing color mode (#4936) 2024-09-24 15:44:55 +01:00
Evgeny Poberezkin d6dc35738e core: 6.0.5.0 (simplexmq 6.0.5.0) 2024-09-24 12:42:22 +01:00
Evgeny 5b3aba9db2 ci: dont build when files in core do not change (#4797) 2024-09-24 12:40:58 +01:00
Diogo 4526afe7e9 desktop: wrap content of remote hosts on overflow (#4923)
* desktop: wrap content of remote hosts on overflow

* fix long device text and align

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-24 11:51:02 +01:00
Evgeny Poberezkin c67302a5bb core: update simplexmq 2024-09-24 09:51:21 +01:00
Evgeny Poberezkin 3685c85743 ios: reduce scroll stickiness threshold for user profiles to 32px 2024-09-24 09:46:54 +01:00
Evgeny 0f301adc57 core: xrcp encryption with forward secrecy (#4926)
* core: xrcp encryption with forward secrecy (tests intermittently fail)

* track and correlate keys

* simplify

* refactor

* remove comment
2024-09-24 09:25:41 +01:00
Stanislav Dmitrenko 1f226dda64 android: fix status bar color after hiding call (#4928)
* android: fix status bar color after hiding call

* dark status bar in call
2024-09-24 09:25:14 +01:00
Stanislav Dmitrenko 981cbb8bf9 android: target API level 34 (Android 14) (#4697) 2024-09-23 09:05:14 +01:00
Stanislav Dmitrenko d5507f2fa3 android, desktop: member name position depends on length (#4918)
* android, desktop: member name position depends on length

* maxWidth limit

* fix

* optimization

* paddings

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-23 08:57:59 +01:00
Narasimha-sc 55d180466a docs: iOS notifications in FAQ (#4879)
* docs: iOS notifications in FAQ

* Update FAQ.md
2024-09-21 21:34:13 +01:00
Arturs Krumins 8a906485d1 ios: display year in chat for previous years (#4919)
* ios: display year in chat for previous years

* fix chat time, show past years in the list

* style

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-21 21:33:18 +01:00
Arturs Krumins e79fa136a4 ios: fix keyboard loosing focus when forward search results are empty (#4895) 2024-09-21 19:26:42 +01:00
Evgeny 560b521673 ios: scrolling user profiles (#4909)
* ios: scrolling user profiles

---------

Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com>
2024-09-21 19:12:53 +01:00
Evgeny c849f5356d core: save the time user profile was opened at to order in ui (#4920)
* core: save the time user profile was opened at to order in ui

* replace timestamp with order
2024-09-21 13:07:27 +01:00
Evgeny Poberezkin 33e12e35a0 ios: use translation in dropdown 2024-09-20 21:19:22 +01:00
spaced4ndy 8a70bad9af core: process ERRS event (#4896)
* core: process ERRS event

* refactor

* update

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-20 21:13:27 +04:00
Evgeny Poberezkin efb7fc6c3b 6.1-beta.1: ios 238, android 240, desktop 67 2024-09-20 15:01:47 +01:00
Evgeny 630fea42c3 website: translations (#4916)
* ios: update core library

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (257 of 257 strings)

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

* Revert "ios: update core library"

This reverts commit 211f6c51f2.

---------

Co-authored-by: summoner001 <summoner@vivaldi.net>
2024-09-20 12:47:40 +01:00
Evgeny 68e570656d ui: translations (#4915)
* ios: update core library

* Translated using Weblate (Portuguese)

Currently translated at 46.2% (944 of 2041 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2041 of 2041 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% (2041 of 2041 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 99.2% (1787 of 1800 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 99.2% (1787 of 1800 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2041 of 2041 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1800 of 1800 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2041 of 2041 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 99.9% (1799 of 1800 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2041 of 2041 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Japanese)

Currently translated at 88.6% (1809 of 2041 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 38.5% (786 of 2041 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 38.8% (793 of 2041 strings)

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

* Translated using Weblate (German)

Currently translated at 99.9% (2040 of 2041 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1800 of 1800 strings)

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

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

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 90.0% (1847 of 2051 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 39.4% (810 of 2051 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2051 of 2051 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% (2051 of 2051 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% (2067 of 2067 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% (1800 of 1800 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 40.0% (827 of 2067 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.3% (2054 of 2067 strings)

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

* Translated using Weblate (Hungarian)

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

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

* ios: update core library

* Translated using Weblate (Portuguese)

Currently translated at 46.2% (944 of 2041 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2041 of 2041 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% (2041 of 2041 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 99.2% (1787 of 1800 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 99.2% (1787 of 1800 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2041 of 2041 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1800 of 1800 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2041 of 2041 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 99.9% (1799 of 1800 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2041 of 2041 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Japanese)

Currently translated at 88.6% (1809 of 2041 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 38.5% (786 of 2041 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 38.8% (793 of 2041 strings)

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

* Translated using Weblate (German)

Currently translated at 99.9% (2040 of 2041 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1800 of 1800 strings)

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

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

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 90.0% (1847 of 2051 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2051 of 2051 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 39.4% (810 of 2051 strings)

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

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2051 of 2051 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% (2051 of 2051 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% (2067 of 2067 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% (1800 of 1800 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 40.0% (827 of 2067 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (2067 of 2067 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.3% (2054 of 2067 strings)

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

* Translated using Weblate (Hungarian)

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

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

* process localizations

* fix interpolation

---------

Co-authored-by: Antonio Oliveira <antoniovini47@gmail.com>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Miyu Sakatsuki <miyu-sakatsuki@outlook.jp>
Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
2024-09-20 12:34:38 +01:00
Evgeny Poberezkin 3cbb2c2d71 core: 6.1.0.3 2024-09-20 09:45:30 +01:00
spaced4ndy 5ca27f63e6 core: send errors processing (#4910)
* core: send errors processing

* test
2024-09-20 09:27:14 +01:00
Evgeny Poberezkin 63d1b2060e Merge branch 'stable' 2024-09-19 18:01:51 +01:00
Stanislav Dmitrenko 25fb099c44 android, desktop: fix always connecting state in call (#4911) 2024-09-19 18:00:08 +01:00
Diogo d69e222b7b android, desktop: bulk forward (#4868)
---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-19 15:39:14 +01:00
Evgeny Poberezkin f7cb6f2796 Revert "android, desktop: compose 1.6.11 (#4902)"
This reverts commit acc9be1a5b.
2024-09-19 14:10:07 +01:00
Stanislav Dmitrenko c5813b3489 android, desktop: catch URI creation errors (#4901)
* android, desktop: catch URI creation errors

* showing alert when pasting an incorrect link

* moved from Uri to String while processing SimpleX links
2024-09-19 09:36:54 +01:00
spaced4ndy 10ded1530c desktop: cleanup current invitation when closing new chat sheet by pressing on center modal (#4904) 2024-09-19 08:14:50 +01:00
Arturs Krumins 255538e5d7 ios: bulk forward (#4857)
* ios: forward multiple messages

* ios: batch previews, when sending media messsages (#4861)

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Arturs Krumins <auth@levitatingpineapple.com>
Co-authored-by: Diogo <diogofncunha@gmail.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-19 08:04:19 +01:00
Stanislav Dmitrenko acc9be1a5b android, desktop: compose 1.6.11 (#4902) 2024-09-19 07:57:37 +01:00
Diogo d47ff3597d android, desktop: reset incognito on profile picker (#4903) 2024-09-18 19:16:32 +04:00
Evgeny Poberezkin 529921e16a 6.1-beta.0: ios 237, android 239, desktop 66 2024-09-18 11:37:32 +01:00
Evgeny Poberezkin 0a4667304c ios: update core library 2024-09-18 10:18:06 +01:00
Evgeny Poberezkin 17a0f3a210 core: 6.1.0.2, update min versions for remote access to 6.1.0.2 2024-09-18 08:56:43 +01:00
Evgeny Poberezkin c13e8e4037 ios: update core library 2024-09-18 07:28:06 +01:00
Evgeny Poberezkin 3a61290730 core: 6.1.0.1 2024-09-17 22:30:24 +01:00
Evgeny Poberezkin 166082c021 Merge branch 'stable' 2024-09-17 22:23:58 +01:00
Stanislav Dmitrenko 69bbe0ae91 android, desktop: proxy configuration includes credentials (#4892)
* android, desktop: proxy configuration includes credentials

* migration

* changes for disabled socks

* migration

* port

* new logic

* migration

* check validity of fields

* validity of host

* import changes proxy just in case

* send port always

* non-nullable

* Revert "send port always"

This reverts commit 14dd066d80.

* string

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-17 21:09:50 +01:00
spaced4ndy 0d9ef4e567 ui: support CRChatItemsStatusesUpdated response (#4890)
* ui: support new CRChatItemsStatusesUpdated api

* ios

* refactor
2024-09-17 20:51:18 +01:00
spaced4ndy 17b55c51c5 core: update statuses of all batched messages on SENT, RCVD (#4888)
* core: update statuses of all batched messages on SENT, RCVD

* wip

* update all

* refactor
2024-09-17 20:50:26 +01:00
Evgeny 665d9dcd00 ios: SOCKS proxy UI (#4893)
* ios: SOCKS proxy UI

* update network config

* proxy

* adapt

* move, dont default to localhost:9050

* move socks proxy to defaults

* sock proxy preference

* rename

* rename

* fix

* fix

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-17 17:34:24 +01:00
Stanislav Dmitrenko c13c7baaaf desktop: less distance from edge to call icon (#4898) 2024-09-17 17:07:29 +01:00
Evgeny af993529f9 core: migrate SOCKS proxy settings (#4894) 2024-09-17 12:23:50 +01:00
Evgeny Poberezkin 05aab35a1f ios: update core library 2024-09-16 18:23:59 +01:00
Evgeny 40e93cc61e core: reduce max message sizes (#4882)
* core: reduce max message sizes

* reduce

* comment
2024-09-16 18:05:09 +01:00
Evgeny ea320f531f Translated using Weblate (Spanish) (#4891)
Currently translated at 100.0% (257 of 257 strings)

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

Co-authored-by: Eraorahan <eraorahan@gmail.com>
2024-09-16 16:53:11 +01:00
Evgeny 4f99075b14 ui: translations (#4889)
* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 36.4% (741 of 2032 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Indonesian)

Currently translated at 5.8% (118 of 2032 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 36.4% (741 of 2032 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Indonesian)

Currently translated at 5.8% (118 of 2032 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Indonesian)

Currently translated at 6.3% (130 of 2032 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (2032 of 2032 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1785 of 1785 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1785 of 1785 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 83.5% (1697 of 2032 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 12.9% (263 of 2032 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2032 of 2032 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 37.1% (755 of 2032 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 37.8% (769 of 2032 strings)

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

* Translated using Weblate (Czech)

Currently translated at 88.7% (1804 of 2032 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 38.4% (782 of 2032 strings)

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

* process localizations

* fix links

---------

Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com>
Co-authored-by: rbasliana <rbasliana@protonmail.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: re me <jisizhang001@gmail.com>
Co-authored-by: Mateus Pereira <desktecc.union091@passinbox.com>
Co-authored-by: rbasliana <91536894+rbasliana@users.noreply.github.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
2024-09-16 16:50:37 +01:00
Arturs Krumins de7882c904 ios: update user profile sheet design (#4871)
* ios: update user profile sheet design

* revert views

* improve validation

* minor

* align with create profile

* alert on dismiss

* revert x appearance

* update size

* move the fullname

* focus on appear

* profile image

* localizations

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-16 13:28:45 +01:00
Evgeny Poberezkin 0cad378bc5 core: 6.1.0.0 (simplexmq 6.1.0.0) 2024-09-16 13:17:18 +01:00
Evgeny Poberezkin da058ca376 Merge branch 'stable' 2024-09-16 08:09:53 +01:00
Evgeny c22d23750f core: support different SOCKS proxy authentication modes (#4886)
* core: support different SOCKS proxy authentication modes

* use defaultSocksProxyWithAuth

* hostMode CLI option

* simplexmq
2024-09-16 07:33:48 +01:00
Evgeny b5f91f2d31 core: test forwarding image preview without attached file (#4877) 2024-09-14 10:57:55 +01:00
spaced4ndy c6ab8ec6b3 core: cleanup empty file on error; check file status on forward (#4878) 2024-09-13 23:16:23 +04:00
Stanislav Dmitrenko 4447b66b4e android: fix showing logs from core (#4880) 2024-09-13 20:02:32 +01:00
Stanislav Dmitrenko 2539255957 android: remove Worker provider's interface implementation (#4874) 2024-09-13 12:35:12 +01:00
Evgeny 4cfda91124 core: fix ForwardConfirmation encoding (#4872) 2024-09-13 09:35:11 +01:00
Evgeny Poberezkin 7aec147cec core: update simplexmq 6.0.4.0 2024-09-12 22:19:17 +01:00
Evgeny f6f2044675 core: plan forwarding chat items, api types (#4865)
* core: plan forwarding chat items, api types

* remove empty content, refactor get items

* another refactor

* plan

* test

* more tests

* text
2024-09-12 15:21:29 +01:00
Stanislav Dmitrenko 5f0b5c5a9f desktop: fix vlc dependency (2) (#4869) 2024-09-12 14:51:57 +01:00
Evgeny Poberezkin dfdb4af646 Revert "core: bulk forward missing files error handling (#4860)"
This reverts commit 46d774a822.
2024-09-12 08:52:09 +01:00
Narasimha-sc 2ab5f14119 docs: remove outdated latest version number from downloads (#4854) 2024-09-11 21:31:05 +01:00
Diogo 46d774a822 core: bulk forward missing files error handling (#4860)
* add types

* wip dump

* collect errors

* Update src/Simplex/Chat/View.hs

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

* test with not received files

* remove ciFileLoaded

* undo refactoring

* test for skipping missing file with text

* add test for empty message

* remove fdescribes

* copy or cleanup files after collecting errors and forward reqs

* don't forward w/t content

* translate CIFSRcvAborted into FFENotAccepted

* refactor

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-09-11 21:30:09 +01:00
Diogo acf2f1fbbe android, desktop: new user picker (#4796)
* user picker as modal

* android dirty layout

* color mode switcher

* close picker on desktop opening modals

* cleanup

* remote hosts working

* icon buttons

* profile picker modal for external shares

* remove stroke

* color changes

* add unread badge to users row

* chat database settings section

* safe remove of you settings section

* picker should now open for single user

* remove create profile from settings

* paddings

* handle big names

* fonts and align

* simple animations and shadow

* address messaging

* active is grey

* padding

* hide non active devices from pills

* picker positioning

* pills order

* change view all profiles icon

* bigger space between profiles

* hosts ordering and fixes

* device pill in app bar

* simplex address -> public

* better switch of opacity bg

* create public address

* font match

* add icon for dark mode

* padding

* profile name as header

* h2 is too big

* icon colors

* icons

* settings as modal

* center settings

* fix use from desktop

* remove logs

* bar colors

* remove drawer unused code

* animate shading

* fade in fade out

* add system mode toast

* shading colors

* stop pushing shade up

* same button as ios for opening all profiles

* simplify nav bar color set

* broken transition change

* color mix

* gradient and horizontal scroll

* separate title

* align avatars to top

* picker should always remain open

* use chevron icon to see all profiles

* improvements on status and nav color set

* best case bars switching working

* change bar and shading on theme change

* remove unused var

* reset navbar colors on navigate

* updated icon color

* protect android calls

* desktop menu matching size of right side modals

* remove shading from desktop

* close user picker on settings click in desktop

* bigger profile image smaller gap to name

* fix spacer for row scroll on android

* smaller profile name

* remove unused code

* small refactor

* unused

* move desktop/mobile connection

* close drawer on swipe down 30%

* progress dump on new android design

* paddings in scroller

* gradient

* android paddings

* split inactive user picker between platforms

* move your chat profiles inside android specific

* always show your chat profiles in desktop

* fix profile creation in desktop

* remove unused var

* update android space between badges

* initial desktop design

* center android icons with avatar

* centered avatars

* unread badge

* extra space in the end of user list for android

* aligned paddings on desktop

* desktop paddings

* paddings

* remove you

* unread badge same style as chatlist

* use bedtime moon for dark mode

* chevron same size as sun/moon

* chevron and gradient

* paddings

* split android and desktop scaffold for picker

* move bars logic to android

* remove android check

* more android checks

* initial version of swipable modal

* muted as grey

* unused

* close drawer on 3/4

* better close control

* make all animations match

* move shadow with offset

* always close pciker on selection

* animated float doing nothing

* sync animation

* animation using single float

* fixed warnings

* better state update

* fix scrim color

* better handling of picker closure on desktop

* landscape mode

* intentation

* rename UserPickerScaffold

* hide shadow when picker not open

* reset inactive user scroll position on pick

* unused class

* left panel after new menu can be without padding

* small changes

* make ActiveProfilePicker reusable to reduce code duplication

* make picker scrollable

* refactor

* refactor and fix instant reload of profiles

* refactor

* icon sizes

* returned back ability to scroll to the picker on Android

* setting system theme on desktop's right click

* box

* refactor

* picker pill

* fix desktop shadow

* small change

* hiding keyboard when opening picker

* state specifying

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-11 15:51:28 +01:00
Evgeny Poberezkin 1a853d4eea Merge branch 'stable' 2024-09-11 13:06:11 +01:00
spaced4ndy 3ea8279451 multiplatform: fix delete messages alert (#4862) 2024-09-11 13:04:59 +01:00
spaced4ndy 87c55dbbf7 multiplatform: fix delete messages alert (#4862) 2024-09-11 15:08:16 +04:00
Evgeny fb4475027d ios: new user picker (#4821)
* ios: new user picker (#4770)

* current user picker progress

* one hand picker

* thin bullet icon

* more user picker buttons

* button clickable areas

* divider padding

* extra space after sun

* send current user option to address view

* add unread count badge

* with anim for apperance close

* edit current profile from picker

* remove you section from settings

* remove help and support

* simplify

* move settings and sun to same row

* remove redundant vstack

* long press on sun/moon switches to system setting

* remove back button from migrate device

* smooth profile transitions

* close user picker on list profiles

* fix dismiss on migrate from device

* fix dismiss when deleting last visible user while having hidden users

* picker visibility toggle tweaks

* remove strange square from profile switcher click

* dirty way to save auto accept settings on dismiss

* Revert "dirty way to save auto accept settings on dismiss"

This reverts commit e7b19ee8aa.

* consistent animation on user picker toggle

* change space between profiles

* remove result

* ignore result

* unread badge

* move to sheet

* half sheet on one hand ui

* fix dismiss on device migration

* fix desktop connect

* sun to meet other action icons

* fill bullet list button

* fix tap in settings to take full width

* icon sizings and paddings

* open settings in same sheet

* apply same trick as other buttons for ligth toggle

* layout

* open profiles sheet large when +3 users

* layout

* layout

* paddings

* paddings

* remove show progress

* always small user picker

* fixed height

* open all actions as sheets

* type, color

* simpler and more effective way of avoid moving around on user select

* dismiss user profiles sheet on user change

* connect desktop back button remove

* remove back buttons from user address view

* remove porgress

* header inside list

* alert on auto accept unsaved changes

* Cancel -> Discard

* revert

* fix connect to desktop

* remove extra space

* fix share inside multi sheet

* user picker and options as separate sheet

* revert showShareSheet

* fix current profile and all profiles selection

* change alert

* update

* cleanup user address

* remove func

* alert on unsaved changes in chat prefs

* fix layout

* cleanup

---------

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

* ios: fix switching profiles (#4822)

* ios: different user picker layout (#4826)

* ios: different user picker layout

* remove section

* layout, color

* color

* remove activeUser

* fix gradient

* recursive sheets

* gradient padding

* share sheet

* layout

* dismiss sheets

---------

Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com>

* ios: use the same way to share from all sheets (#4829)

* ios: close user picker before opening other sheets

* Revert "share sheet"

This reverts commit 0064155825.

* dismiss/show via callback

* Revert "ios: close user picker before opening other sheets"

This reverts commit 19110398f8.

* ios: show alerts from sheets (#4839)

* padding

* remove gradient

* cleanup

* simplify settings

* padding

---------

Co-authored-by: Diogo <diogofncunha@gmail.com>
Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-09-10 09:31:53 +01:00
spaced4ndy 388609563d core: update simplexmq (ntf encoding) (#4853) 2024-09-09 18:22:14 +04:00
Evgeny 0cb568d206 fix incorrect error of migration to device (#4852)
* fix incorrect error of migration to device

* alert to finish migration, ios fix

* simplexmq

* catching exception and stopping chat

* text

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-09-09 15:01:18 +01:00
Arturs Krumins 8f6e9741e7 ios: add floating date separator (#4801)
* ios: add floating date separator

* floating date separator

* revert formatTimestampText

* send tuple, reduce lookups

* background date visibility

* whitespace

* streamline

* visible date

* move pipeline to ReverseList

* space

* remove ViewUpdate

* cleanup

* refactor

* combine unread items model updates

* split publisher

* remove readItemPublisher

* revert markChatItemRead_ change

* use single item api

* comment test buttons

* style

* update top floating button instantly

* cleanup

* cleanup

* minor

* remove task

* prevent concurrent updates

* fix mark chat read

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-09 14:58:22 +01:00
Evgeny Poberezkin e00001b571 Merge branch 'stable' 2024-09-08 23:01:54 +01:00
Evgeny 351cfcbcbc core: allow deleting user when user record in agent database was deleted (#4851) 2024-09-08 23:01:31 +01:00
Evgeny 74b837bf9a core: allow deleting user when user record in agent database was deleted (#4851) 2024-09-08 20:02:38 +01:00
Henrique Albuquerque 691cd489ea blog: fix grammar error (#4846) 2024-09-08 12:56:15 +01:00
Evgeny 5ed701402b core: optimize marking chat items as read, batch API (#4843)
* core: optimize marking chat items as read

* tests, ui types

* ios: fix api

* refactor
2024-09-07 19:40:10 +01:00
Evgeny 1839dab17b ios: move caching images to background thread, dont use main thread scheduler for marking items read (#4840) 2024-09-06 22:09:55 +01:00
Arturs Krumins 06939343a1 ios: revert showing date in chat list timestamp (#4834) 2024-09-06 13:32:41 +01:00
Evgeny Poberezkin b2ebb81fcf Merge branch 'stable' 2024-09-06 12:40:16 +01:00
Stanislav Dmitrenko bccbb9900f android: fix initializing WorkManager (#4833) 2024-09-06 12:39:23 +01:00
Arturs Krumins 1f3355921c ios: make message information on media readable (#4820)
* ios: ensure legibility of elements rendered over media

* reduce diff

* match meta padding

* material play background

* remove circlebadge

* progress circle

* meta color modes

* refactor

* conditional space

* fix

* fix2

* fix3

* revert video buttons

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-06 12:36:54 +01:00
Evgeny Poberezkin 9e45f68a6e Merge branch 'stable' 2024-09-05 12:06:11 +01:00
Evgeny ebb5d629e7 ios: attach share sheet to the topmost view controller (to support sharing from nested sheets) (#4830) 2024-09-04 23:12:05 +01:00
Diogo e002f33c53 multiplatform: fix deleted contacts with conversations being identified as contact cards (#4828)
* multiplatform: fix deleted contacts with conversations being identified as contact cards

* fix in ios

* Revert "fix in ios"

This reverts commit 9b8c6bc125.

* fix for ios

* same check as ios for android and desktop
2024-09-04 18:12:41 +01:00
Evgeny 71bea947a5 ios: cache base64 images (#4827) 2024-09-04 14:49:01 +01:00
Evgeny Poberezkin 90d5abdff1 Merge branch 'stable' 2024-09-03 14:39:34 +01:00
Stanislav Dmitrenko 6407d5de63 android, desktop: making list items unique (#4812) 2024-09-03 14:38:42 +01:00
Stanislav Dmitrenko 9cba96082d android, desktop: fix crash when window is small in chat view (#4819) 2024-09-03 14:37:04 +01:00
Stanislav Dmitrenko 2089fd8539 android, desktop: remove slash from strings (#4818) 2024-09-03 14:36:41 +01:00
Stanislav Dmitrenko 31c4ff2705 android, desktop: group info remembers scroll position and search (#4817) 2024-09-03 14:36:01 +01:00
Arturs Krumins 33895b0330 ios: show received messages using checkmark with slash (#4816)
* ios: show received messages using checkmark with slash

* update message info view

* cleanup

* remove dead arguments

* Revert "remove dead arguments"

This reverts commit 1fc07669c7.

* remove status icon

* cleanup

* update assets

* tweak checkmark

* fix space, rename

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-03 07:59:40 +01:00
sh fe20a43232 flatpak: update metainfo (#4811) 2024-09-02 15:15:16 +01:00
Stanislav Dmitrenko 087519c39d desktop: fix vlc dependency (#4809) 2024-09-02 13:08:09 +01:00
Stanislav Dmitrenko 3b49817f17 desktop: fix vlc dependency (#4809) 2024-08-31 18:24:50 +01:00
Evgeny Poberezkin f94f0dea08 website: fix links 2024-08-31 14:34:46 +01:00
Evgeny Poberezkin d68a3ba80d ios: update core library 2024-08-31 11:58:49 +01:00
Evgeny Poberezkin 7a4dca1e4c Merge branch 'stable' 2024-08-31 11:41:35 +01:00
Evgeny fff29f8548 faq: private message routing (#4807)
* faq: private message routing

* readme

* corrections
2024-08-31 11:41:21 +01:00
Evgeny 8cc03b6c21 docs: FAQ on deletion of sent messages and read receipts (#4470)
* docs: FAQ on deletion of sent messages and read receipts

* update
2024-08-31 11:41:06 +01:00
Evgeny 7a5b04d523 faq: private message routing (#4807)
* faq: private message routing

* readme

* corrections
2024-08-31 11:39:43 +01:00
Evgeny Poberezkin d7ab0aef14 Merge branch 'stable' 2024-08-31 08:18:09 +01:00
Evgeny 41cb734d56 docs: FAQ on deletion of sent messages and read receipts (#4470)
* docs: FAQ on deletion of sent messages and read receipts

* update
2024-08-30 21:31:57 +01:00
Evgeny Poberezkin 6adf8f29b0 6.0.4: ios 236, android 237, desktop 65 2024-08-30 19:59:40 +01:00
Stanislav Dmitrenko 4ca1b57e1b android, desktop: small enhancements to new chat sheet (#4803)
* android, desktop: small enhancements to new chat sheet

* padding

* normal view matching stable

* fix one hand layout

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Diogo <diogofncunha@gmail.com>
2024-08-30 18:34:53 +01:00
Evgeny Poberezkin 9432a5e5cd ios: update core library 2024-08-30 17:09:49 +01:00
Evgeny Poberezkin a9ec1f9ec1 core: 6.0.4.0 (simplexmq 6.0.3.0) 2024-08-30 13:39:35 +01:00
Stanislav Dmitrenko 122387d180 android, desktop: fix loading chat items when search was not empty (#4802) 2024-08-30 11:11:26 +01:00
Arturs Krumins 23f54c1022 ios: fix crash regression (#4800) 2024-08-29 18:33:48 +01:00
Arturs Krumins 0b0b78293f ios: fix inaccurate floating unread counters in chat message view (#4781)
* ios: fix inaccurate floating unread counters in chat message view

* account for inset; remove old on appear/disappear blocks

* revert id

* first visible

* remove UnreadChatItemCounts

* cleanup

* revert duplicates

* add todo

* throttle first

* cleanup

* lines

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-29 17:25:08 +01:00
Evgeny eef1e97ecc ci: dont build when files in core do not change (#4797) 2024-08-29 13:40:55 +01:00
Evgeny Poberezkin 564b137f95 Merge branch 'stable' 2024-08-29 13:16:07 +01:00
Stanislav Dmitrenko 6edea46dad android, desktop: improvement to a lock UI (#4769)
* android, desktop: improvement to a lock UI

* oneTime passcode screen which allows to pass verification while in call

* change

* unused line

* don't ask to set up auth if already has

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-29 13:15:11 +01:00
Diogo 2fe3acf4df fix android simulator build (#4795) 2024-08-29 12:01:29 +01:00
Arturs Krumins 1c64b17545 ios: remove tails from group invitations (#4792) 2024-08-29 11:19:41 +01:00
Evgeny 700918f0ca ios: show member role on the right (#4783)
* ios: show member role on the right

* member layout

---------

Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com>
2024-08-28 20:55:54 +01:00
Evgeny Poberezkin 94c552ca12 Merge branch 'stable' 2024-08-28 18:04:50 +01:00
Stanislav Dmitrenko dfe16991d0 ios: make CallKit calls fire in time after cold start (#4787)
* ios: make CallKit calls fire in time after cold start

* longer wait period

* uncomment

* change

* change

* removed commented code

* ios: update core library

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-28 15:49:11 +01:00
Stanislav Dmitrenko acb372a4ce core: call uuid (#4777)
* core: call uuid

* fix

* text

* android, desktop

* ios

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-28 15:31:02 +01:00
sh 121eaf6073 flatpak: update metainfo (#4784)
* flatpak: update metainfo

* flatpak: change release link and ol to ul
2024-08-28 10:39:28 +01:00
Evgeny 8cc075eda8 ios: show correct message times (#4779) 2024-08-27 22:13:20 +01:00
Diogo 05e7f35037 core: fix associated agent user for recreated connections (#4771)
* core: fix associated user for recreated connections

* fix test for connection recreation
2024-08-27 22:12:55 +01:00
Diogo e582d2d742 android, desktop: allow for chat profile selection on new chat screen (#4741)
* add api and types

* basic ui

* add search on profiles

* profile images on select chat profile

* incognito adjustments

* basic api connection

* handling errors

* add loading state

* header to scroll

* selected profile on top (profile or incognito)

* adjust share profile copy

* avoid list moving around on selection commit

* bigger profile pick

* info icon interactive area

* thumbs to match contacts list size

* incognito sizes matching icons

* title to section padding

* add chevron

* align borders and other chevron icon

* prevent click on self

* only prevent selection

* update

* selectable item area

* no need for oninfo to be composable

* simplify

* wrap apis in try

* remove redundant derivedStateOf

* closure fns capital naming

* simplify current user null check

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-27 14:32:54 +01:00
Arturs Krumins 043a4ed915 ios: add chat message tail and roundness settings; date separators (#4764)
* ios: add chat message tail and roundness settings

* cleanup

* minor

* rename

* date separator

* revert max roundness to pills

* increase default roundness to 1

* minor

* out of bounds tails, style date separator

* formatting

* hardcode tail growth

* revert

* different shape (WIP)

* tail

* rename

* square

* only show tail for the last message

* remove func

* capture less

* variable tail height

* export localizations

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-27 14:30:07 +01:00
Stanislav Dmitrenko 76cb9013f5 desktop: show only AppImage download option for those who running AppImage (#4774) 2024-08-27 11:21:00 +01:00
Stanislav Dmitrenko f1e8c65aa1 android, desktop: using SemVer when checking for updates (#4768)
* android, desktop: using SemVer when checking for updates

* simplify

* simplify

* no comment

* simplify

* change

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-26 21:06:21 +01:00
Evgeny Poberezkin 8a6bf05773 Merge branch 'stable' 2024-08-26 20:06:28 +01:00
Stanislav Dmitrenko 0118e64ab4 android, desktop: items padding and min height (#4767) 2024-08-26 16:59:57 +01:00
Arturs Krumins 0477b1aad3 ios: time based message grouping (#4743)
* ios: time based message grouping

* cleanup

* hide timestamp

* fix chat item not getting updated

* round to minute

* separate by minute

* chat dir

* time separation struct

* add date logic

* cleanup

* fix groups

* simplify timestamp logic; remove shape

* cleanup

* cleanup

* refactor, add type

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-25 19:21:24 +01:00
Evgeny Poberezkin ae850c8ce8 ios: update core library 2024-08-25 18:00:46 +01:00
Evgeny Poberezkin 94ae571ec3 Merge branch 'stable' 2024-08-25 14:33:19 +01:00
Evgeny 4552860345 ios: remove unnecessary protocols (#4763) 2024-08-25 14:33:06 +01:00
Evgeny a1579810bb ios: remove unnecessary protocols (#4763) 2024-08-25 14:31:26 +01:00
Evgeny Poberezkin 4574198990 Merge branch 'stable' 2024-08-24 19:14:28 +01:00
Evgeny 4d18174b11 ui: fix Debug delivery (#4757) 2024-08-24 19:10:30 +01:00
Evgeny Poberezkin defd095a4f 6.0.3: ios 235, android 235, desktop 64 2024-08-24 16:32:32 +01:00
Evgeny Poberezkin ed60f28e56 ios: update core library 2024-08-24 15:44:08 +01:00
Evgeny Poberezkin f0b889ffcf core: 6.0.3.0 (simplexmq 6.0.2.0) 2024-08-24 15:07:32 +01:00
Stanislav Dmitrenko c07df9e05f android: target API level 34 (Android 14) (#4697) 2024-08-24 15:00:56 +01:00
Diogo efe8ed1739 ios: fix possible race between incognito set and profile change in conn profile picker (#4752)
* ios: fix possible race between incognito set and profile change in conn profile picker

* typo

* fix swithcing incognito on same profile
2024-08-24 14:59:50 +01:00
spaced4ndy bcd50019be core: add more multi send api tests (#4750) 2024-08-23 21:05:37 +04:00
Evgeny Poberezkin 7b48c59f9f ios: update core library 2024-08-23 14:32:16 +01:00
Diogo 04033fc0b5 ios: connection profile search, incognito info in selection list and improved loader (#4744)
* remove comment

* improve switching chat profile loader

* add search on profile selection

* disable auto correction

* add incognito info in select chat profile

* fix typos

* layout

* fix choosing hidden user

* opacity back

* Revert "layout"

This reverts commit 10f1e5e924.

* remove padding

* selected profile on top (profile or incognito)

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-23 13:20:07 +01:00
Evgeny Poberezkin 7b90e01b3a core: 6.1.0.0 2024-08-23 13:18:51 +01:00
spaced4ndy ef1897f865 multiplatform: multi send & forward api (#4745) 2024-08-22 21:39:13 +04:00
spaced4ndy f587179045 ios: multi send & forward api (#4739) 2024-08-22 21:38:22 +04:00
spaced4ndy 791489e943 core: multi forward api (#4704) 2024-08-22 21:36:35 +04:00
Diogo c485837910 ios: allow for chat profile selection on new chat screen (#4729)
* ios: allow for chat profile selection on new chat screen

* add api and types

* initial api connection with error handling

* improve incognito handling

* adjustments to different server connections

* loading state

* simpler handling of race

* smaller delay

* improve error handling and messages

* fix header

* remove tap section footer

* incognito adjustments

* set UI driving vars in main thread

* remove result

* incognito in profile picker and footer

* put incognito mask inside a circle

* fix click on incognito when already selected

* fix avoid users swapping position when picker is active

* fix pending contact cleanup logic

* icons

* restore incognito help

* fix updating qr code

* remove info from footer

* layout

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-08-22 15:02:32 +01:00
Evgeny Poberezkin a95415fa1a 6.0.2: ios 234, android 234, desktop 63 2024-08-22 12:22:28 +01:00
Evgeny 8d48c4b14c ui: translations (#4740)
* Translated using Weblate (Italian)

Currently translated at 100.0% (2032 of 2032 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1785 of 1785 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 64.6% (1314 of 2032 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 62.8% (1121 of 1785 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (2032 of 2032 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Vietnamese)

Currently translated at 33.5% (682 of 2032 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 81.6% (1659 of 2032 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 81.6% (1659 of 2032 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 62.8% (1121 of 1785 strings)

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 49.6% (887 of 1785 strings)

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

* Translated using Weblate (Polish)

Currently translated at 99.9% (2031 of 2032 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1785 of 1785 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 5.3% (108 of 2032 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 34.2% (696 of 2032 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 85.4% (1736 of 2032 strings)

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1785 of 1785 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 86.8% (1764 of 2032 strings)

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

* Translated using Weblate (Czech)

Currently translated at 88.6% (1801 of 2032 strings)

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

* Translated using Weblate (Indonesian)

Currently translated at 5.8% (118 of 2032 strings)

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* Translated using Weblate (Vietnamese)

Currently translated at 34.4% (701 of 2032 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 34.9% (710 of 2032 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Arabic)

Currently translated at 100.0% (2032 of 2032 strings)

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

* Translated using Weblate (Vietnamese)

Currently translated at 35.6% (725 of 2032 strings)

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

* Translated using Weblate (Hungarian)

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

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

* Translated using Weblate (Spanish)

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

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

* process localizations

* ru

* ru export

* hu

---------

Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: kumo <cloud_029@icloud.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: M1K4 <oomikaoo@gmail.com>
Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com>
Co-authored-by: Jiaa <jorden2895@gmail.com>
Co-authored-by: axmfs <axmfs@proton.me>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: billy appetie <billy_appetie@users.noreply.hosted.weblate.org>
Co-authored-by: Max <Prototypem95@users.noreply.hosted.weblate.org>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
2024-08-22 11:40:54 +01:00
Stanislav Dmitrenko 9f44242e4c ios: fix applying theme when no global theme edited yet (#4738)
* ios: fix applying theme when no global theme editer yet

* base color

* added check for base

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-22 08:34:17 +01:00
Evgeny Poberezkin 7e7d93c596 ios: update library 2024-08-21 22:43:24 +01:00
Stanislav Dmitrenko 073818db55 android, desktop: fix applying theme when no global theme edited yet (#4735) 2024-08-21 22:05:50 +01:00
Stanislav Dmitrenko 519dd9e219 android, desktop: fix onboarding layout (#4734) 2024-08-21 22:03:49 +01:00
Evgeny Poberezkin 94218a1a7e core: 6.0.2.0 (simplexmq 6.0.1.0) 2024-08-21 19:20:20 +01:00
Arturs Krumins 996c6efddd ios: prevent hangs when opening app from background with async api calls (#4730)
* ios: async api calls on entering foreground

* rename

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-21 19:16:57 +01:00
Arturs Krumins fd9c080103 ios: fix last message being hidden on load (#4733) 2024-08-21 17:04:12 +01:00
Stanislav Dmitrenko 5cb8badb22 android, desktop: layout changes for settings items (#4732)
* android, desktop: layout changes for settings items

* section paddings

* toggle

* padding and border

* padding

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-21 16:46:42 +01:00
Arturs Krumins 0438f35539 ios: fix notifying the recipient when hanging up a call in ios15 (#4731)
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-21 15:05:59 +01:00
Diogo d5eb7b7811 core: api to change user of pending connections (#4681)
* core: add api that enables change of owner user id for pending connections

* old user sends request, incognito handling and coverage

* call agent inside set connection api

* only set user id if servers match

* simplify

* reduce test noise

* return invitation when a newone is created

* add test for profile on different server

* refactor namings

* update simplexmq

* refactor

* test improvements and simplify

* remove fdescribes

* simplify and reduce vars scope

* put if back

* refactor, change error

* refactor view

* refactor

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-08-21 13:27:58 +04:00
Evgeny Poberezkin e04f74738e Revert "ios: asynchronous api calls when entering foreground (#4710)"
This reverts commit b52dfee078.
2024-08-20 18:24:53 +01:00
Stanislav Dmitrenko 5f0ccb9f17 ios: fix interface color without callKit (#4727)
* ios: fix interface color without callKit

* call area

* better hiding sheet when making a call without CallKit

* invert condition
2024-08-20 17:04:00 +01:00
Arturs Krumins b2d18f6960 ios: fix visual artifacts when opening a chat on iOS15 (#4726)
* ios: fix visual artifacts when opening a chat on iOS15

* fix separators

* cleanup
2024-08-20 13:54:52 +01:00
Arturs Krumins b52dfee078 ios: asynchronous api calls when entering foreground (#4710)
* ios: get call invitations asynchronously

* async update chats

* async user list on appear

* move model changes to main thread

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-20 11:28:06 +01:00
Evgeny 70991debfd ios: optimize performance of updating chats (#4723)
* ios: optimize performance of updating chats

* even simpler

* always use updateChats

* make chats readonly from outside of model
2024-08-20 09:29:52 +01:00
Stanislav Dmitrenko 885aa9cfa5 android, desktop: scrolling moves title to app bar (#4703)
* android, desktop: scrolling moves title to app bar

* one place should be without padding

* scroll related changes for both platforms

* adapt code to universal ColumnWithScrollBar

* show in center

* small adjustments

* new chat sheet fix

* divider + mix background color for desktop

* coerce

* different transition

* desktop title starts from left

* host starts from left too

* different coefficient

* settings title
2024-08-19 19:43:54 +01:00
Evgeny 75a468434c core: only start message delivery workers when there are pending messages (#4713)
* core: use threads instead of async (reduce memory)

* simplexmq

* core: only start message delivery workers when there are pending messages (#4714)

* core: only start message delivery workers when there are pending messages

* update tls

* simplexmq

* update ios, simplexmq

* simplexmq
2024-08-18 23:00:34 +01:00
Arturs Krumins 3b98032371 ios: filter agent subscriptions from chat console (#4708) 2024-08-17 15:52:35 +01:00
603 changed files with 32438 additions and 9281 deletions
+11 -1
View File
@@ -5,12 +5,22 @@ on:
branches:
- master
- stable
- users
tags:
- "v*"
- "!*-fdroid"
- "!*-armv7a"
pull_request:
paths-ignore:
- "apps/ios"
- "apps/multiplatform"
- "blog"
- "docs"
- "fastlane"
- "images"
- "packages"
- "website"
- "README.md"
- "PRIVACY.md"
jobs:
prepare-release:
+1
View File
@@ -61,6 +61,7 @@ website/package/generated*
# Ignore build tool output, e.g. code coverage
website/.nyc_output/
website/coverage/
result
# Ignore API documentation
website/api-docs/
+20 -18
View File
@@ -72,7 +72,7 @@ You must:
Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment.
You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform:
@@ -83,7 +83,7 @@ There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=s
There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users:
[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaJ8O1O8A8GbeoaHTo_V8dcefaCl7ouPb%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA034qWTA3sWcTsi6aWhNf9BA34vKVCFaEBdP2R66z6Ao%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22wiZ1v_wNjLPlT-nCSB-bRA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FmfiivxDKWFuowXrQOp11jsY8TuP__rBL%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAiz3pKNwvKudckFYMUfgoT0s96B0jfZ7ALHAu7rtE9HQ%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22jZeJpXGrRXQJU_-MSJ_v2A%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FJ5ES83pJimY2BRklS8fvy_iQwIU37xra%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0F0STP6UqN_12_k2cjjTrIjFgBGeWhOAmbY1qlk3pnM%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22VmUU0fqmYdCRmVCyvStvHA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FxCHBE_6PBRMqNEpm4UQDHXb9cz-mN7dd%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAetqlcM7zTCRw-iatnwCrvpJSto7lq5Yv6AsBMWv7GSM%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22foO5Xw4hhjOa_x7zET7otw%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FVXQTB0J2lLjYkgjWByhl6-1qmb5fgZHh%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAI6JaEWezfSwvcoTEkk6au-gkjrXR2ew2OqZYMYBvayk%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22ORH9OEe8Duissh-hslfeVg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FqpHu0psOUdYfc11yQCzSyq5JhijrBzZT%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEACZ_7fbwlM45wl6cGif8cY47oPQ_AMdP0ATqOYLA6zHY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%229uRQRTir3ealdcSfB0zsrw%3D%3D%22%7D) (Italian-speaking).
You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
@@ -300,25 +300,27 @@ What is already implemented:
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses [pairwise per-queue identifiers](./docs/GLOSSARY.md#pairwise-pseudonymous-identifier) (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
2. [End-to-end encryption](./docs/GLOSSARY.md#end-to-end-encryption) in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation.
3. [Double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (each message is encrypted by its own ephemeral key) and [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message.
4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
5. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks.
6. All message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed.
7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
12. Manual messaging queue rotations to move conversation to another SMP relay.
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
14. Local files encryption.
4. [Post-quantum resistant key exchange](./docs/GLOSSARY.md#post-quantum-cryptography) in double ratchet protocol *on every ratchet step*. Read more in [this post](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) and also see this [publication by Apple]( https://security.apple.com/blog/imessage-pq3/) explaining the need for post-quantum key rotation.
5. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
6. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks.
7. All message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed.
8. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
9. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
10. To protect your IP address from unknown messaging relays, and for per-message transport anonymity (compared with Tor/VPN per-connection anonymity), from v6.0 all SimpleX Chat clients use private message routing by default. Read more in [this post](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md#private-message-routing).
11. To protect your IP address from unknown file relays, when SOCKS proxy is not enabled SimpleX Chat clients ask for a confirmation before downloading the files from unknown servers.
12. To protect your IP address from known servers all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
13. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
14. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
15. Manual messaging queue rotations to move conversation to another SMP relay.
16. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
17. Local files encryption.
We plan to add:
1. Senders' SMP relays and recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party.
2. Post-quantum resistant key exchange in double ratchet protocol.
3. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
4. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
5. Reproducible builds this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code.
1. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
2. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
3. Reproducible builds this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code.
4. Recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party.
## For developers
-5
View File
@@ -15,16 +15,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
logger.debug("AppDelegate: didFinishLaunchingWithOptions")
application.registerForRemoteNotifications()
NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil)
removePasscodesIfReinstalled()
prepareForLaunch()
return true
}
@objc func pasteboardChanged() {
ChatModel.shared.pasteboardHasStrings = UIPasteboard.general.hasStrings
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02hhx", $0) }.joined()
logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)")
@@ -0,0 +1,12 @@
{
"info": {
"author": "xcode",
"version": 1
},
"symbols": [
{
"filename": "checkmark.2.svg",
"idiom": "universal"
}
]
}
@@ -0,0 +1,227 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="3300px" height="2200px" viewBox="0 0 3300 2200" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>checkmark.2</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="double.checkmark">
<g id="Notes">
<rect id="artboard" fill="#FFFFFF" fill-rule="nonzero" x="0" y="0" width="3300" height="2200"></rect>
<line x1="263" y1="292" x2="3036" y2="292" id="Path" stroke="#000000" stroke-width="0.5"></line>
<text id="Weight/Scale-Variations" fill="#000000" fill-rule="nonzero" font-family="Helvetica"
font-size="13" font-weight="normal">
<tspan x="263" y="322">Weight/Scale Variations</tspan>
</text>
<text id="Ultralight" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="533.711" y="322">Ultralight</tspan>
</text>
<text id="Thin" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="843.422" y="322">Thin</tspan>
</text>
<text id="Light" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="1138.63" y="322">Light</tspan>
</text>
<text id="Regular" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="1426.84" y="322">Regular</tspan>
</text>
<text id="Medium" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="1723.06" y="322">Medium</tspan>
</text>
<text id="Semibold" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="2015.77" y="322">Semibold</tspan>
</text>
<text id="Bold" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="2326.48" y="322">Bold</tspan>
</text>
<text id="Heavy" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="2618.19" y="322">Heavy</tspan>
</text>
<text id="Black" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="2917.4" y="322">Black</tspan>
</text>
<line x1="263" y1="1903" x2="3036" y2="1903" id="Path" stroke="#000000" stroke-width="0.5"></line>
<g id="Group" transform="translate(264.3672, 1918.0684)" fill="#000000" fill-rule="nonzero">
<path
d="M7.88088,15.76172 C12.18752,15.76172 15.7715,12.1875 15.7715,7.88086 C15.7715,3.57422 12.17774,0 7.8711,0 C3.57422,0 0,3.57422 0,7.88086 C0,12.1875 3.58398,15.76172 7.88086,15.76172 L7.88088,15.76172 Z M7.88088,14.277344 C4.33596,14.277344 1.50392,11.435544 1.50392,7.880864 C1.50392,4.326184 4.32618,1.484384 7.8711,1.484384 C11.42578,1.484384 14.27734,4.326184 14.27734,7.880864 C14.27734,11.435544 11.43554,14.277344 7.88086,14.277344 L7.88088,14.277344 Z M4.28712,7.880864 C4.28712,8.310552 4.589854,8.60352 5.039074,8.60352 L7.138674,8.60352 L7.138674,10.72266 C7.138674,11.162114 7.431642,11.464848 7.86133,11.464848 C8.310548,11.464848 8.603518,11.162114 8.603518,10.72266 L8.603518,8.60352 L10.722658,8.60352 C11.162112,8.60352 11.464846,8.310552 11.464846,7.880864 C11.464846,7.44141 11.162112,7.138676 10.722658,7.138676 L8.603518,7.138676 L8.603518,5.029296 C8.603518,4.580078 8.31055,4.277342 7.86133,4.277342 C7.431642,4.277342 7.138674,4.580076 7.138674,5.029296 L7.138674,7.138676 L5.039074,7.138676 C4.589856,7.138676 4.28712,7.44141 4.28712,7.880864 Z"
id="Shape"></path>
</g>
<g id="Group" transform="translate(283.254, 1915.9883)" fill="#000000" fill-rule="nonzero">
<path
d="M9.96094,19.92188 C15.41016,19.92188 19.92188,15.4004 19.92188,9.96094 C19.92188,4.51172 15.4004,0 9.95118,0 C4.51172,0 0,4.51172 0,9.96094 C0,15.4004 4.52148,19.92188 9.96094,19.92188 Z M9.96094,18.261724 C5.35156,18.261724 1.66992,14.570324 1.66992,9.960944 C1.66992,5.351564 5.3418,1.660164 9.95116,1.660164 C14.56052,1.660164 18.2617,5.351564 18.2617,9.960944 C18.2617,14.570324 14.5703,18.261724 9.96092,18.261724 L9.96094,18.261724 Z M5.4297,9.960944 C5.4297,10.43946 5.761732,10.761726 6.259778,10.761726 L9.130878,10.761726 L9.130878,13.642586 C9.130878,14.130868 9.46291,14.472664 9.941424,14.472664 C10.4297,14.472664 10.771502,14.140632 10.771502,13.642586 L10.771502,10.761726 L13.652362,10.761726 C14.140644,10.761726 14.48244,10.43946 14.48244,9.960944 C14.48244,9.472662 14.140644,9.130866 13.652362,9.130866 L10.771502,9.130866 L10.771502,6.259766 C10.771502,5.76172 10.4297,5.419922 9.941424,5.419922 C9.462908,5.419922 9.130878,5.761718 9.130878,6.259766 L9.130878,9.130866 L6.259778,9.130866 C5.761732,9.130866 5.4297,9.472662 5.4297,9.960944 Z"
id="Shape"></path>
</g>
<g id="Group" transform="translate(307.1798, 1913.2246)" fill="#000000" fill-rule="nonzero">
<path
d="M12.71486,25.43944 C19.67776,25.43944 25.43946,19.67772 25.43946,12.7246 C25.43946,5.7617 19.66798,0 12.70508,0 C5.75196,0 -1.42108547e-15,5.76172 -1.42108547e-15,12.7246 C-1.42108547e-15,19.67772 5.76172,25.43944 12.71484,25.43944 L12.71486,25.43944 Z M12.71486,23.623034 C6.6797,23.623034 1.82618,18.759754 1.82618,12.724594 C1.82618,6.679674 6.66994,1.826154 12.70508,1.826154 C18.75,1.826154 23.61328,6.679674 23.61328,12.724594 C23.61328,18.759754 18.75976,23.623034 12.71484,23.623034 L12.71486,23.623034 Z M6.94338,12.724594 C6.94338,13.242172 7.314474,13.6035 7.861348,13.6035 L11.806668,13.6035 L11.806668,17.55858 C11.806668,18.09569 12.177762,18.476548 12.69534,18.476548 C13.23245,18.476548 13.603544,18.105454 13.603544,17.55858 L13.603544,13.6035 L17.558624,13.6035 C18.095734,13.6035 18.476592,13.242172 18.476592,12.724594 C18.476592,12.177718 18.105498,11.806626 17.558624,11.806626 L13.603544,11.806626 L13.603544,7.861306 C13.603544,7.31443 13.23245,6.933572 12.69534,6.933572 C12.177762,6.933572 11.806668,7.314432 11.806668,7.861306 L11.806668,11.806626 L7.861348,11.806626 C7.314472,11.806626 6.94338,12.17772 6.94338,12.724594 Z"
id="Shape"></path>
</g>
<text id="Design-Variations" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="263" y="1953">Design Variations</tspan>
</text>
<text id="Symbols-are-supported-in-up-to-nine-weights-and-three-scales." fill="#000000"
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="263" y="1971">Symbols are supported in up to nine weights and three scales.</tspan>
</text>
<text id="For-optimal-layout-with-text-and-other-symbols,-vertically-align" fill="#000000"
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="263" y="1989">For optimal layout with text and other symbols, vertically align</tspan>
</text>
<text id="symbols-with-the-adjacent-text." fill="#000000" fill-rule="nonzero" font-family="Helvetica"
font-size="13" font-weight="normal">
<tspan x="263" y="2007">symbols with the adjacent text.</tspan>
</text>
<line x1="776" y1="1919" x2="776" y2="1933" id="Path" stroke="#00AEEF" stroke-width="0.5"></line>
<g id="Group" transform="translate(778.4902, 1918.7324)" fill="#000000" fill-rule="nonzero">
<path
d="M0.8203116,14.423832 C1.3378896,14.423832 1.5917956,14.2285116 1.7773436,13.681636 L3.0371096,10.234376 L8.7988296,10.234376 L10.0585956,13.681636 C10.2441424,14.228512 10.4980496,14.423832 11.0058616,14.423832 C11.5234396,14.423832 11.8554716,14.111324 11.8554716,13.623042 C11.8554716,13.4570264 11.8261748,13.300776 11.7480498,13.095698 L7.1679698,0.898438 C6.9433598,0.302734 6.5429698,0 5.9179698,0 C5.3125018,0 4.9023458,0.292968 4.6875018,0.888672 L0.1074218,13.105472 C0.0292968,13.31055 -3.55271368e-16,13.4668 -3.55271368e-16,13.632816 C-3.55271368e-16,14.121098 0.3125,14.423832 0.820312,14.423832 L0.8203116,14.423832 Z M3.5156316,8.750004 L5.8886716,2.177744 L5.9374998,2.177744 L8.3105398,8.750004 L3.5156316,8.750004 Z"
id="Shape"></path>
</g>
<line x1="792.836" y1="1919" x2="792.836" y2="1933" id="Path" stroke="#00AEEF" stroke-width="0.5">
</line>
<text id="Margins" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="776" y="1953">Margins</tspan>
</text>
<text id="Leading-and-trailing-margins-on-the-left-and-right-side-of-each-symbol" fill="#000000"
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="776" y="1971">Leading and trailing margins on the left and right side of each symbol
</tspan>
</text>
<text id="can-be-adjusted-by-modifying-the-x-location-of-the-margin-guidelines." fill="#000000"
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="776" y="1989">can be adjusted by modifying the x-location of the margin guidelines.
</tspan>
</text>
<text id="Modifications-are-automatically-applied-proportionally-to-all" fill="#000000"
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="776" y="2007">Modifications are automatically applied proportionally to all</tspan>
</text>
<text id="scales-and-weights." fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="776" y="2025">scales and weights.</tspan>
</text>
<g id="Group" transform="translate(1291.2481, 1914.5174)" fill="#000000" fill-rule="nonzero">
<path
d="M0.593687825,20.3477978 L2.29290583,22.0567818 C3.15228183,22.9259218 4.13860983,22.8673278 5.06634583,21.8419378 L15.7597058,10.0548378 L14.7929098,9.07827578 L4.17766983,20.7579558 C3.82610783,21.1583458 3.49407583,21.2560018 3.02532583,20.7872526 L1.85344983,19.6251426 C1.38469983,19.1661586 1.49212183,18.8243606 1.89251223,18.4630326 L13.3671122,7.66225258 L12.3905502,6.69545658 L0.798750225,17.5841366 C-0.187577775,18.5021046 -0.265703775,19.4786686 0.593672225,20.3478166 L0.593687825,20.3477978 Z M7.00970783,2.15443778 C6.58978583,2.56459378 6.56048983,3.14076578 6.79486383,3.53139178 C7.02923983,3.89271978 7.48822383,4.12709578 8.13275383,3.96107978 C9.59759783,3.61928378 11.1210338,3.56068978 12.5468138,4.49818978 L11.9608758,5.95326778 C11.6190798,6.78334578 11.7948602,7.36928378 12.3319698,7.91615778 L14.6268898,10.2306178 C15.1151718,10.7188998 15.5253278,10.7384298 16.0917338,10.6407738 L17.1561878,10.4454614 L17.8202498,11.1192894 L17.7811874,11.6759294 C17.742125,12.1739754 17.869078,12.5548354 18.3573594,13.0333514 L19.1190774,13.7755394 C19.5975934,14.2540554 20.2128274,14.2833514 20.6815774,13.8146018 L23.5917374,10.8946818 C24.0604874,10.4259318 24.0409554,9.83022778 23.5624406,9.35171378 L22.7909566,8.58999578 C22.3124406,8.11147978 21.9413466,7.95522978 21.4628326,7.99429178 L20.8866606,8.04311998 L20.2421286,7.40835398 L20.4862686,6.28530798 C20.6132218,5.71890198 20.4569718,5.27944798 19.8710346,4.69351198 L17.6737746,2.50601198 C14.3339346,-0.814308021 9.90033463,-0.736168021 7.00971463,2.15444998 L7.00970783,2.15443778 Z M8.50384783,2.52553178 C10.9354878,0.748187779 14.2265078,1.05092178 16.4530678,3.27748578 L18.8847078,5.68958578 C19.1190838,5.92396178 19.1581458,6.10950778 19.0897858,6.45130378 L18.7675198,7.93567978 L20.2714258,9.42005578 L21.2577538,9.36146198 C21.5116598,9.35169636 21.5897858,9.3712276 21.7850978,9.56653998 L22.3612698,10.142712 L19.9198698,12.584112 L19.3436978,12.00794 C19.1483854,11.8126276 19.1190878,11.734502 19.1288538,11.47083 L19.1972132,10.494268 L17.7030732,9.00989198 L16.1796352,9.26379798 C15.8573692,9.33215738 15.7108852,9.30286038 15.4667452,9.06848558 L13.4647852,7.06652558 C13.2108792,6.83214958 13.1815812,6.66613558 13.337832,6.29504158 L14.216738,4.20520158 C12.654238,2.75012358 10.622978,2.12512158 8.59173803,2.72082558 C8.43548803,2.75988798 8.37689403,2.63293498 8.50384743,2.52551318 L8.50384783,2.52553178 Z"
id="Shape"></path>
</g>
<text id="Exporting" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="1289" y="1953">Exporting</tspan>
</text>
<text id="Symbols-should-be-outlined-when-exporting-to-ensure-the" fill="#000000" fill-rule="nonzero"
font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="1289" y="1971">Symbols should be outlined when exporting to ensure the</tspan>
</text>
<text id="design-is-preserved-when-submitting-to-Xcode." fill="#000000" fill-rule="nonzero"
font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="1289" y="1989">design is preserved when submitting to Xcode.</tspan>
</text>
<text id="template-version" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="2952" y="1933">Template v.5.0</tspan>
</text>
<text id="Requires-Xcode-15-or-greater" fill="#000000" fill-rule="nonzero" font-family="Helvetica"
font-size="13" font-weight="normal">
<tspan x="2865" y="1951">Requires Xcode 15 or greater</tspan>
</text>
<text id="descriptive-name" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="2835" y="1969">Generated from double.checkmark</tspan>
</text>
<text id="Typeset-at-100.0-points" fill="#000000" fill-rule="nonzero" font-family="Helvetica"
font-size="13" font-weight="normal">
<tspan x="2901" y="1987">Typeset at 100.0 points</tspan>
</text>
<text id="Small" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="263" y="726">Small</tspan>
</text>
<text id="Medium" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="263" y="1156">Medium</tspan>
</text>
<text id="Large" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="263" y="1586">Large</tspan>
</text>
</g>
<g id="Guides" transform="translate(263, 600.785)">
<g id="H-reference" transform="translate(76.9937, 24.756)" fill="#27AAE1" fill-rule="nonzero">
<path
d="M0,70.459 L2.644096,70.459 L28.334446,3.3267 L29.036646,3.3267 L29.036646,0 L27.128946,0 L0,70.459 Z M10.694846,45.9791 L45.987846,45.9791 L45.237846,43.7305 L11.444846,43.7305 L10.694846,45.9791 Z M54.125946,70.459 L56.770046,70.459 L29.644546,0 L28.438946,0 L28.438946,3.3267 L54.125946,70.459 Z"
id="Shape"></path>
</g>
<line x1="0" y1="95.215" x2="2773" y2="95.215" id="Baseline-S" stroke="#27AAE1" stroke-width="0.5">
</line>
<line x1="0" y1="24.756" x2="2773" y2="24.756" id="Capline-S" stroke="#27AAE1" stroke-width="0.5">
</line>
<g id="H-reference" transform="translate(76.9937, 454.756)" fill="#27AAE1" fill-rule="nonzero">
<path
d="M0,70.459 L2.644096,70.459 L28.334446,3.3267 L29.036646,3.3267 L29.036646,0 L27.128946,0 L0,70.459 Z M10.694846,45.9791 L45.987846,45.9791 L45.237846,43.7305 L11.444846,43.7305 L10.694846,45.9791 Z M54.125946,70.459 L56.770046,70.459 L29.644546,0 L28.438946,0 L28.438946,3.3267 L54.125946,70.459 Z"
id="Shape"></path>
</g>
<line x1="0" y1="525.215" x2="2773" y2="525.215" id="Baseline-M" stroke="#27AAE1" stroke-width="0.5">
</line>
<line x1="0" y1="454.755" x2="2773" y2="454.755" id="Capline-M" stroke="#27AAE1" stroke-width="0.5">
</line>
<g id="H-reference" transform="translate(76.9937, 884.756)" fill="#27AAE1" fill-rule="nonzero">
<path
d="M0,70.459 L2.644096,70.459 L28.334446,3.3267 L29.036646,3.3267 L29.036646,0 L27.128946,0 L0,70.459 Z M10.694846,45.9791 L45.987846,45.9791 L45.237846,43.7305 L11.444846,43.7305 L10.694846,45.9791 Z M54.125946,70.459 L56.770046,70.459 L29.644546,0 L28.438946,0 L28.438946,3.3267 L54.125946,70.459 Z"
id="Shape"></path>
</g>
<line x1="0" y1="955.215" x2="2773" y2="955.215" id="Baseline-L" stroke="#27AAE1" stroke-width="0.5">
</line>
<line x1="0" y1="884.755" x2="2773" y2="884.755" id="Capline-L" stroke="#27AAE1" stroke-width="0.5">
</line>
<line x1="256.625" y1="1.13686838e-13" x2="256.625" y2="119.336" id="left-margin-Ultralight-S"
stroke="#00AEEF" stroke-width="0.5"></line>
<line x1="348.798" y1="1.13686838e-13" x2="348.798" y2="119.336" id="right-margin-Ultralight-S"
stroke="#00AEEF" stroke-width="0.5"></line>
<line x1="1143.53" y1="1.13686838e-13" x2="1143.53" y2="119.336" id="left-margin-Regular-S"
stroke="#00AEEF" stroke-width="0.5"></line>
<line x1="1257.15" y1="1.13686838e-13" x2="1257.15" y2="119.336" id="right-margin-Regular-S"
stroke="#00AEEF" stroke-width="0.5"></line>
<line x1="2622.62" y1="1.13686838e-13" x2="2622.62" y2="119.336" id="left-margin-Black-S"
stroke="#00AEEF" stroke-width="0.5"></line>
<line x1="2760.18" y1="1.13686838e-13" x2="2760.18" y2="119.336" id="right-margin-Black-S"
stroke="#00AEEF" stroke-width="0.5"></line>
</g>
<g id="Symbols" transform="translate(529.3906, 625.2969)" stroke="#000000" stroke-width="0.5">
<g id="Black-S" transform="translate(2365.995, 0)">
<path
d="M67.46878,71.191381 C71.17968,71.191381 74.06058,69.873022 76.01368,66.99216 L111.07228,15.2343 C112.43948,13.2324 113.02538,11.1328 113.02538,9.2773 C113.02538,4.0039 108.82618,0 103.35738,0 C99.69528,0 97.30278,1.3183 95.05668,4.834 L67.32228,47.9492 L53.69918,32.6172 C51.79488,30.4687 49.54888,29.4433 46.52148,29.4433 C41.05278,29.4433 37,33.4472 37,38.7695 C37,41.2109 37.63478,43.1152 39.73438,45.459 L59.36328,67.67576 C61.51168,70.117162 64.14848,71.191381 67.46878,71.191381 Z"
id="Path"></path>
<path
d="M9.52148,29.4433 C12.54888,29.4433 14.79488,30.4687 16.69918,32.6172 L30.32228,47.9492 L32.291,44.888 L44.484,58.915 L39.01368,66.99216 C37.1235832,69.780091 34.3645791,71.1046997 30.825305,71.1872572 L30.46878,71.191381 C27.14848,71.191381 24.51168,70.117162 22.36328,67.67576 L2.73438,45.459 C0.63478,43.1152 0,41.2109 0,38.7695 C0,33.4472 4.05278,29.4433 9.52148,29.4433 Z M66.35738,0 C71.82618,0 76.02538,4.0039 76.02538,9.2773 C76.02538,11.1328 75.43948,13.2324 74.07228,15.2343 L61.951,33.129 L49.252,18.52 L58.05668,4.834 C60.2386057,1.41874857 62.5586852,0.0771252245 66.0465687,0.00324899359 Z"
id="Combined-Shape"></path>
</g>
<g id="Regular-S" transform="translate(886.905, 3.7109)">
<path
d="M55.87888,66.113294 C57.78318,66.113294 59.29688,65.28322 60.37108,63.62306 L95.96678,7.3242 C96.79688,6.0547 97.08988,5.0293 97.08988,4.0039 C97.08988,1.6113 95.52738,0 93.08598,0 C91.32818,0 90.35158,0.586 89.27738,2.2949 L55.68358,56.2012 L38.00778,32.3731 C36.88478,30.8594 35.81058,30.2246 34.19918,30.2246 C31.75778,30.2246 30,31.9336 30,34.375 C30,35.4004 30.43948,36.5234 31.26958,37.5977 L51.24028,63.5254 C52.60738,65.28322 53.97458,66.113294 55.87888,66.113294 Z"
id="Path"></path>
<path
d="M4.19918,30.2246 C5.81058,30.2246 6.88478,30.8594 8.00778,32.3731 L25.68358,56.2012 L30.332,48.741 L35.554,55.426 L30.37108,63.62306 C29.3457073,65.2077582 27.919881,66.0361177 26.1361316,66.1081489 L25.87888,66.113294 C23.97458,66.113294 22.60738,65.28322 21.24028,63.5254 L1.26958,37.5977 C0.43948,36.5234 0,35.4004 0,34.375 C0,31.9336 1.75778,30.2246 4.19918,30.2246 Z M63.08598,0 C65.52738,0 67.08988,1.6113 67.08988,4.0039 C67.08988,5.0293 66.79688,6.0547 65.96678,7.3242 L48.893,34.328 L43.564,27.508 L59.27738,2.2949 C60.3068217,0.657204167 61.2466272,0.0507828125 62.8702602,0.00309092159 Z"
id="Combined-Shape"></path>
</g>
<g id="Ultralight-S" transform="translate(0, 4.4375)">
<path
d="M36.79298,62.07178 C37.24418,62.07178 37.53178,61.87744 37.78868,61.53417 L76.33598,2.0112 C76.57578,1.6045 76.64168,1.3965 76.64168,1.1885 C76.64168,0.5215 76.12358,0 75.45318,0 C75.01228,0 74.71678,0.1772 74.50538,0.5693 L36.73398,58.97114 L18.24078,37.7768 C17.98048,37.3984 17.67828,37.2177 17.20218,37.2177 C16.48638,37.2177 16,37.7006 16,38.371 C16,38.6699 16.12159,38.9755 16.40678,39.2778 L35.65088,61.52733 C36.01908,61.96826 36.29638,62.07178 36.79298,62.07178 Z"
id="Path"></path>
<path
d="M1.20218,37.2177 C1.67828,37.2177 1.98048,37.3984 2.24078,37.7768 L20.73398,58.97114 L23.078,55.345 L24.636,57.137 L21.78868,61.53417 C21.5603244,61.8392989 21.3077121,62.0267547 20.937503,62.0646445 L20.79298,62.07178 C20.29638,62.07178 20.01908,61.96826 19.65088,61.52733 L0.40678,39.2778 C0.12159,38.9755 0,38.6699 0,38.371 C0,37.7006 0.48638,37.2177 1.20218,37.2177 Z M59.45318,0 C60.12358,0 60.64168,0.5215 60.64168,1.1885 C60.64168,1.3965 60.57578,1.6045 60.33598,2.0112 L32.216,45.432 L30.652,43.634 L58.50538,0.5693 C58.6932911,0.220766667 58.9476516,0.0420308642 59.3115144,0.00661467764 Z"
id="Combined-Shape"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,12 @@
{
"info": {
"author": "xcode",
"version": 1
},
"symbols": [
{
"filename": "checkmark.wide.svg",
"idiom": "universal"
}
]
}
@@ -0,0 +1,218 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="3300px" height="2200px" viewBox="0 0 3300 2200" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<title>checkmark.wide</title>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="double.checkmark">
<g id="Notes">
<rect id="artboard" fill="#FFFFFF" fill-rule="nonzero" x="0" y="0" width="3300" height="2200"></rect>
<line x1="263" y1="292" x2="3036" y2="292" id="Path" stroke="#000000" stroke-width="0.5"></line>
<text id="Weight/Scale-Variations" fill="#000000" fill-rule="nonzero" font-family="Helvetica"
font-size="13" font-weight="normal">
<tspan x="263" y="322">Weight/Scale Variations</tspan>
</text>
<text id="Ultralight" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="533.711" y="322">Ultralight</tspan>
</text>
<text id="Thin" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="843.422" y="322">Thin</tspan>
</text>
<text id="Light" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="1138.63" y="322">Light</tspan>
</text>
<text id="Regular" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="1426.84" y="322">Regular</tspan>
</text>
<text id="Medium" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="1723.06" y="322">Medium</tspan>
</text>
<text id="Semibold" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="2015.77" y="322">Semibold</tspan>
</text>
<text id="Bold" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="2326.48" y="322">Bold</tspan>
</text>
<text id="Heavy" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="2618.19" y="322">Heavy</tspan>
</text>
<text id="Black" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="2917.4" y="322">Black</tspan>
</text>
<line x1="263" y1="1903" x2="3036" y2="1903" id="Path" stroke="#000000" stroke-width="0.5"></line>
<g id="Group" transform="translate(264.3672, 1918.0684)" fill="#000000" fill-rule="nonzero">
<path
d="M7.88088,15.76172 C12.18752,15.76172 15.7715,12.1875 15.7715,7.88086 C15.7715,3.57422 12.17774,0 7.8711,0 C3.57422,0 0,3.57422 0,7.88086 C0,12.1875 3.58398,15.76172 7.88086,15.76172 L7.88088,15.76172 Z M7.88088,14.277344 C4.33596,14.277344 1.50392,11.435544 1.50392,7.880864 C1.50392,4.326184 4.32618,1.484384 7.8711,1.484384 C11.42578,1.484384 14.27734,4.326184 14.27734,7.880864 C14.27734,11.435544 11.43554,14.277344 7.88086,14.277344 L7.88088,14.277344 Z M4.28712,7.880864 C4.28712,8.310552 4.589854,8.60352 5.039074,8.60352 L7.138674,8.60352 L7.138674,10.72266 C7.138674,11.162114 7.431642,11.464848 7.86133,11.464848 C8.310548,11.464848 8.603518,11.162114 8.603518,10.72266 L8.603518,8.60352 L10.722658,8.60352 C11.162112,8.60352 11.464846,8.310552 11.464846,7.880864 C11.464846,7.44141 11.162112,7.138676 10.722658,7.138676 L8.603518,7.138676 L8.603518,5.029296 C8.603518,4.580078 8.31055,4.277342 7.86133,4.277342 C7.431642,4.277342 7.138674,4.580076 7.138674,5.029296 L7.138674,7.138676 L5.039074,7.138676 C4.589856,7.138676 4.28712,7.44141 4.28712,7.880864 Z"
id="Shape"></path>
</g>
<g id="Group" transform="translate(283.254, 1915.9883)" fill="#000000" fill-rule="nonzero">
<path
d="M9.96094,19.92188 C15.41016,19.92188 19.92188,15.4004 19.92188,9.96094 C19.92188,4.51172 15.4004,0 9.95118,0 C4.51172,0 0,4.51172 0,9.96094 C0,15.4004 4.52148,19.92188 9.96094,19.92188 Z M9.96094,18.261724 C5.35156,18.261724 1.66992,14.570324 1.66992,9.960944 C1.66992,5.351564 5.3418,1.660164 9.95116,1.660164 C14.56052,1.660164 18.2617,5.351564 18.2617,9.960944 C18.2617,14.570324 14.5703,18.261724 9.96092,18.261724 L9.96094,18.261724 Z M5.4297,9.960944 C5.4297,10.43946 5.761732,10.761726 6.259778,10.761726 L9.130878,10.761726 L9.130878,13.642586 C9.130878,14.130868 9.46291,14.472664 9.941424,14.472664 C10.4297,14.472664 10.771502,14.140632 10.771502,13.642586 L10.771502,10.761726 L13.652362,10.761726 C14.140644,10.761726 14.48244,10.43946 14.48244,9.960944 C14.48244,9.472662 14.140644,9.130866 13.652362,9.130866 L10.771502,9.130866 L10.771502,6.259766 C10.771502,5.76172 10.4297,5.419922 9.941424,5.419922 C9.462908,5.419922 9.130878,5.761718 9.130878,6.259766 L9.130878,9.130866 L6.259778,9.130866 C5.761732,9.130866 5.4297,9.472662 5.4297,9.960944 Z"
id="Shape"></path>
</g>
<g id="Group" transform="translate(307.1798, 1913.2246)" fill="#000000" fill-rule="nonzero">
<path
d="M12.71486,25.43944 C19.67776,25.43944 25.43946,19.67772 25.43946,12.7246 C25.43946,5.7617 19.66798,0 12.70508,0 C5.75196,0 -1.42108547e-15,5.76172 -1.42108547e-15,12.7246 C-1.42108547e-15,19.67772 5.76172,25.43944 12.71484,25.43944 L12.71486,25.43944 Z M12.71486,23.623034 C6.6797,23.623034 1.82618,18.759754 1.82618,12.724594 C1.82618,6.679674 6.66994,1.826154 12.70508,1.826154 C18.75,1.826154 23.61328,6.679674 23.61328,12.724594 C23.61328,18.759754 18.75976,23.623034 12.71484,23.623034 L12.71486,23.623034 Z M6.94338,12.724594 C6.94338,13.242172 7.314474,13.6035 7.861348,13.6035 L11.806668,13.6035 L11.806668,17.55858 C11.806668,18.09569 12.177762,18.476548 12.69534,18.476548 C13.23245,18.476548 13.603544,18.105454 13.603544,17.55858 L13.603544,13.6035 L17.558624,13.6035 C18.095734,13.6035 18.476592,13.242172 18.476592,12.724594 C18.476592,12.177718 18.105498,11.806626 17.558624,11.806626 L13.603544,11.806626 L13.603544,7.861306 C13.603544,7.31443 13.23245,6.933572 12.69534,6.933572 C12.177762,6.933572 11.806668,7.314432 11.806668,7.861306 L11.806668,11.806626 L7.861348,11.806626 C7.314472,11.806626 6.94338,12.17772 6.94338,12.724594 Z"
id="Shape"></path>
</g>
<text id="Design-Variations" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="263" y="1953">Design Variations</tspan>
</text>
<text id="Symbols-are-supported-in-up-to-nine-weights-and-three-scales." fill="#000000"
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="263" y="1971">Symbols are supported in up to nine weights and three scales.</tspan>
</text>
<text id="For-optimal-layout-with-text-and-other-symbols,-vertically-align" fill="#000000"
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="263" y="1989">For optimal layout with text and other symbols, vertically align</tspan>
</text>
<text id="symbols-with-the-adjacent-text." fill="#000000" fill-rule="nonzero" font-family="Helvetica"
font-size="13" font-weight="normal">
<tspan x="263" y="2007">symbols with the adjacent text.</tspan>
</text>
<line x1="776" y1="1919" x2="776" y2="1933" id="Path" stroke="#00AEEF" stroke-width="0.5"></line>
<g id="Group" transform="translate(778.4902, 1918.7324)" fill="#000000" fill-rule="nonzero">
<path
d="M0.8203116,14.423832 C1.3378896,14.423832 1.5917956,14.2285116 1.7773436,13.681636 L3.0371096,10.234376 L8.7988296,10.234376 L10.0585956,13.681636 C10.2441424,14.228512 10.4980496,14.423832 11.0058616,14.423832 C11.5234396,14.423832 11.8554716,14.111324 11.8554716,13.623042 C11.8554716,13.4570264 11.8261748,13.300776 11.7480498,13.095698 L7.1679698,0.898438 C6.9433598,0.302734 6.5429698,0 5.9179698,0 C5.3125018,0 4.9023458,0.292968 4.6875018,0.888672 L0.1074218,13.105472 C0.0292968,13.31055 -3.55271368e-16,13.4668 -3.55271368e-16,13.632816 C-3.55271368e-16,14.121098 0.3125,14.423832 0.820312,14.423832 L0.8203116,14.423832 Z M3.5156316,8.750004 L5.8886716,2.177744 L5.9374998,2.177744 L8.3105398,8.750004 L3.5156316,8.750004 Z"
id="Shape"></path>
</g>
<line x1="792.836" y1="1919" x2="792.836" y2="1933" id="Path" stroke="#00AEEF" stroke-width="0.5">
</line>
<text id="Margins" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="776" y="1953">Margins</tspan>
</text>
<text id="Leading-and-trailing-margins-on-the-left-and-right-side-of-each-symbol" fill="#000000"
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="776" y="1971">Leading and trailing margins on the left and right side of each symbol
</tspan>
</text>
<text id="can-be-adjusted-by-modifying-the-x-location-of-the-margin-guidelines." fill="#000000"
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="776" y="1989">can be adjusted by modifying the x-location of the margin guidelines.
</tspan>
</text>
<text id="Modifications-are-automatically-applied-proportionally-to-all" fill="#000000"
fill-rule="nonzero" font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="776" y="2007">Modifications are automatically applied proportionally to all</tspan>
</text>
<text id="scales-and-weights." fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="776" y="2025">scales and weights.</tspan>
</text>
<g id="Group" transform="translate(1291.2481, 1914.5174)" fill="#000000" fill-rule="nonzero">
<path
d="M0.593687825,20.3477978 L2.29290583,22.0567818 C3.15228183,22.9259218 4.13860983,22.8673278 5.06634583,21.8419378 L15.7597058,10.0548378 L14.7929098,9.07827578 L4.17766983,20.7579558 C3.82610783,21.1583458 3.49407583,21.2560018 3.02532583,20.7872526 L1.85344983,19.6251426 C1.38469983,19.1661586 1.49212183,18.8243606 1.89251223,18.4630326 L13.3671122,7.66225258 L12.3905502,6.69545658 L0.798750225,17.5841366 C-0.187577775,18.5021046 -0.265703775,19.4786686 0.593672225,20.3478166 L0.593687825,20.3477978 Z M7.00970783,2.15443778 C6.58978583,2.56459378 6.56048983,3.14076578 6.79486383,3.53139178 C7.02923983,3.89271978 7.48822383,4.12709578 8.13275383,3.96107978 C9.59759783,3.61928378 11.1210338,3.56068978 12.5468138,4.49818978 L11.9608758,5.95326778 C11.6190798,6.78334578 11.7948602,7.36928378 12.3319698,7.91615778 L14.6268898,10.2306178 C15.1151718,10.7188998 15.5253278,10.7384298 16.0917338,10.6407738 L17.1561878,10.4454614 L17.8202498,11.1192894 L17.7811874,11.6759294 C17.742125,12.1739754 17.869078,12.5548354 18.3573594,13.0333514 L19.1190774,13.7755394 C19.5975934,14.2540554 20.2128274,14.2833514 20.6815774,13.8146018 L23.5917374,10.8946818 C24.0604874,10.4259318 24.0409554,9.83022778 23.5624406,9.35171378 L22.7909566,8.58999578 C22.3124406,8.11147978 21.9413466,7.95522978 21.4628326,7.99429178 L20.8866606,8.04311998 L20.2421286,7.40835398 L20.4862686,6.28530798 C20.6132218,5.71890198 20.4569718,5.27944798 19.8710346,4.69351198 L17.6737746,2.50601198 C14.3339346,-0.814308021 9.90033463,-0.736168021 7.00971463,2.15444998 L7.00970783,2.15443778 Z M8.50384783,2.52553178 C10.9354878,0.748187779 14.2265078,1.05092178 16.4530678,3.27748578 L18.8847078,5.68958578 C19.1190838,5.92396178 19.1581458,6.10950778 19.0897858,6.45130378 L18.7675198,7.93567978 L20.2714258,9.42005578 L21.2577538,9.36146198 C21.5116598,9.35169636 21.5897858,9.3712276 21.7850978,9.56653998 L22.3612698,10.142712 L19.9198698,12.584112 L19.3436978,12.00794 C19.1483854,11.8126276 19.1190878,11.734502 19.1288538,11.47083 L19.1972132,10.494268 L17.7030732,9.00989198 L16.1796352,9.26379798 C15.8573692,9.33215738 15.7108852,9.30286038 15.4667452,9.06848558 L13.4647852,7.06652558 C13.2108792,6.83214958 13.1815812,6.66613558 13.337832,6.29504158 L14.216738,4.20520158 C12.654238,2.75012358 10.622978,2.12512158 8.59173803,2.72082558 C8.43548803,2.75988798 8.37689403,2.63293498 8.50384743,2.52551318 L8.50384783,2.52553178 Z"
id="Shape"></path>
</g>
<text id="Exporting" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="1289" y="1953">Exporting</tspan>
</text>
<text id="Symbols-should-be-outlined-when-exporting-to-ensure-the" fill="#000000" fill-rule="nonzero"
font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="1289" y="1971">Symbols should be outlined when exporting to ensure the</tspan>
</text>
<text id="design-is-preserved-when-submitting-to-Xcode." fill="#000000" fill-rule="nonzero"
font-family="Helvetica" font-size="13" font-weight="normal">
<tspan x="1289" y="1989">design is preserved when submitting to Xcode.</tspan>
</text>
<text id="template-version" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="2952" y="1933">Template v.5.0</tspan>
</text>
<text id="Requires-Xcode-15-or-greater" fill="#000000" fill-rule="nonzero" font-family="Helvetica"
font-size="13" font-weight="normal">
<tspan x="2865" y="1951">Requires Xcode 15 or greater</tspan>
</text>
<text id="descriptive-name" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="2835" y="1969">Generated from double.checkmark</tspan>
</text>
<text id="Typeset-at-100.0-points" fill="#000000" fill-rule="nonzero" font-family="Helvetica"
font-size="13" font-weight="normal">
<tspan x="2901" y="1987">Typeset at 100.0 points</tspan>
</text>
<text id="Small" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="263" y="726">Small</tspan>
</text>
<text id="Medium" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="263" y="1156">Medium</tspan>
</text>
<text id="Large" fill="#000000" fill-rule="nonzero" font-family="Helvetica" font-size="13"
font-weight="normal">
<tspan x="263" y="1586">Large</tspan>
</text>
</g>
<g id="Guides" transform="translate(263, 600.785)">
<g id="H-reference" transform="translate(76.9937, 24.756)" fill="#27AAE1" fill-rule="nonzero">
<path
d="M0,70.459 L2.644096,70.459 L28.334446,3.3267 L29.036646,3.3267 L29.036646,0 L27.128946,0 L0,70.459 Z M10.694846,45.9791 L45.987846,45.9791 L45.237846,43.7305 L11.444846,43.7305 L10.694846,45.9791 Z M54.125946,70.459 L56.770046,70.459 L29.644546,0 L28.438946,0 L28.438946,3.3267 L54.125946,70.459 Z"
id="Shape"></path>
</g>
<line x1="0" y1="95.215" x2="2773" y2="95.215" id="Baseline-S" stroke="#27AAE1" stroke-width="0.5">
</line>
<line x1="0" y1="24.756" x2="2773" y2="24.756" id="Capline-S" stroke="#27AAE1" stroke-width="0.5">
</line>
<g id="H-reference" transform="translate(76.9937, 454.756)" fill="#27AAE1" fill-rule="nonzero">
<path
d="M0,70.459 L2.644096,70.459 L28.334446,3.3267 L29.036646,3.3267 L29.036646,0 L27.128946,0 L0,70.459 Z M10.694846,45.9791 L45.987846,45.9791 L45.237846,43.7305 L11.444846,43.7305 L10.694846,45.9791 Z M54.125946,70.459 L56.770046,70.459 L29.644546,0 L28.438946,0 L28.438946,3.3267 L54.125946,70.459 Z"
id="Shape"></path>
</g>
<line x1="0" y1="525.215" x2="2773" y2="525.215" id="Baseline-M" stroke="#27AAE1" stroke-width="0.5">
</line>
<line x1="0" y1="454.755" x2="2773" y2="454.755" id="Capline-M" stroke="#27AAE1" stroke-width="0.5">
</line>
<g id="H-reference" transform="translate(76.9937, 884.756)" fill="#27AAE1" fill-rule="nonzero">
<path
d="M0,70.459 L2.644096,70.459 L28.334446,3.3267 L29.036646,3.3267 L29.036646,0 L27.128946,0 L0,70.459 Z M10.694846,45.9791 L45.987846,45.9791 L45.237846,43.7305 L11.444846,43.7305 L10.694846,45.9791 Z M54.125946,70.459 L56.770046,70.459 L29.644546,0 L28.438946,0 L28.438946,3.3267 L54.125946,70.459 Z"
id="Shape"></path>
</g>
<line x1="0" y1="955.215" x2="2773" y2="955.215" id="Baseline-L" stroke="#27AAE1" stroke-width="0.5">
</line>
<line x1="0" y1="884.755" x2="2773" y2="884.755" id="Capline-L" stroke="#27AAE1" stroke-width="0.5">
</line>
<line x1="256.625" y1="1.13686838e-13" x2="256.625" y2="119.336" id="left-margin-Ultralight-S"
stroke="#00AEEF" stroke-width="0.5"></line>
<line x1="348.798" y1="1.13686838e-13" x2="348.798" y2="119.336" id="right-margin-Ultralight-S"
stroke="#00AEEF" stroke-width="0.5"></line>
<line x1="1143.53" y1="1.13686838e-13" x2="1143.53" y2="119.336" id="left-margin-Regular-S"
stroke="#00AEEF" stroke-width="0.5"></line>
<line x1="1257.15" y1="1.13686838e-13" x2="1257.15" y2="119.336" id="right-margin-Regular-S"
stroke="#00AEEF" stroke-width="0.5"></line>
<line x1="2622.62" y1="1.13686838e-13" x2="2622.62" y2="119.336" id="left-margin-Black-S"
stroke="#00AEEF" stroke-width="0.5"></line>
<line x1="2760.18" y1="1.13686838e-13" x2="2760.18" y2="119.336" id="right-margin-Black-S"
stroke="#00AEEF" stroke-width="0.5"></line>
</g>
<g id="Symbols" transform="translate(529.3906, 625.2969)" stroke="#000000" stroke-width="0.5">
<g id="Black-S" transform="translate(2365.995, 0)">
<path
d="M30.46878,71.191381 C34.17968,71.191381 37.06058,69.873022 39.01368,66.99216 L74.07228,15.2343 C75.43948,13.2324 76.02538,11.1328 76.02538,9.2773 C76.02538,4.0039 71.82618,0 66.35738,0 C62.69528,0 60.30278,1.3183 58.05668,4.834 L30.32228,47.9492 L16.69918,32.6172 C14.79488,30.4687 12.54888,29.4433 9.52148,29.4433 C4.05278,29.4433 0,33.4472 0,38.7695 C0,41.2109 0.63478,43.1152 2.73438,45.459 L22.36328,67.67576 C24.51168,70.117162 27.14848,71.191381 30.46878,71.191381 Z"
id="Path"></path>
</g>
<g id="Regular-S" transform="translate(886.905, 3.7109)">
<path
d="M25.87888,66.113294 C27.78318,66.113294 29.29688,65.28322 30.37108,63.62306 L65.96678,7.3242 C66.79688,6.0547 67.08988,5.0293 67.08988,4.0039 C67.08988,1.6113 65.52738,0 63.08598,0 C61.32818,0 60.35158,0.586 59.27738,2.2949 L25.68358,56.2012 L8.00778,32.3731 C6.88478,30.8594 5.81058,30.2246 4.19918,30.2246 C1.75778,30.2246 0,31.9336 0,34.375 C0,35.4004 0.43948,36.5234 1.26958,37.5977 L21.24028,63.5254 C22.60738,65.28322 23.97458,66.113294 25.87888,66.113294 Z"
id="Path"></path>
</g>
<g id="Ultralight-S" transform="translate(0, 4.4375)">
<path
d="M20.79298,62.07178 C21.24418,62.07178 21.53178,61.87744 21.78868,61.53417 L60.33598,2.0112 C60.57578,1.6045 60.64168,1.3965 60.64168,1.1885 C60.64168,0.5215 60.12358,0 59.45318,0 C59.01228,0 58.71678,0.1772 58.50538,0.5693 L20.73398,58.97114 L2.24078,37.7768 C1.98048,37.3984 1.67828,37.2177 1.20218,37.2177 C0.48638,37.2177 0,37.7006 0,38.371 C0,38.6699 0.12159,38.9755 0.40678,39.2778 L19.65088,61.52733 C20.01908,61.96826 20.29638,62.07178 20.79298,62.07178 Z"
id="Path"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

+19 -13
View File
@@ -29,14 +29,14 @@ struct ContentView: View {
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
@State private var showSettings = false
@State private var showWhatsNew = false
@State private var showChooseLAMode = false
@State private var showSetPasscode = false
@State private var waitingForOrPassedAuth = true
@State private var chatListActionSheet: ChatListActionSheet? = nil
@State private var chatListUserPickerSheet: UserPickerSheet? = nil
private let callTopPadding: CGFloat = 50
private let callTopPadding: CGFloat = 40
private enum ChatListActionSheet: Identifiable {
case planAndConnectSheet(sheet: PlanAndConnectActionSheet)
@@ -86,7 +86,7 @@ struct ContentView: View {
callView(call)
}
if !showSettings, let la = chatModel.laRequest {
if chatListUserPickerSheet == nil, let la = chatModel.laRequest {
LocalAuthView(authRequest: la)
.onDisappear {
// this flag is separate from accessAuthenticated to show initializationView while we wait for authentication
@@ -109,9 +109,6 @@ struct ContentView: View {
}
}
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
.sheet(isPresented: $showSettings) {
SettingsView(showSettings: $showSettings)
}
.confirmationDialog("SimpleX Lock mode", isPresented: $showChooseLAMode, titleVisibility: .visible) {
Button("System authentication") { initialEnableLA() }
Button("Passcode entry") { showSetPasscode = true }
@@ -151,12 +148,12 @@ struct ContentView: View {
}
}
.onAppear {
reactOnDarkThemeChanges()
reactOnDarkThemeChanges(systemInDarkThemeCurrently)
}
.onChange(of: colorScheme) { scheme in
// It's needed to update UI colors when iOS wants to make screenshot after going to background,
// so when a user changes his global theme from dark to light or back, the app will adapt to it
reactOnDarkThemeChanges()
reactOnDarkThemeChanges(scheme == .dark)
}
.onChange(of: theme.name) { _ in
ThemeManager.adjustWindowStyle()
@@ -207,7 +204,7 @@ struct ContentView: View {
CallDuration(call: call)
}
.padding(.horizontal)
.frame(height: callTopPadding - 10)
.frame(height: callTopPadding)
.background(Color(uiColor: UIColor(red: 47/255, green: 208/255, blue: 88/255, alpha: 1)))
.onTapGesture {
chatModel.activeCallViewIsCollapsed = false
@@ -253,7 +250,7 @@ struct ContentView: View {
private func mainView() -> some View {
ZStack(alignment: .top) {
ChatListView(showSettings: $showSettings).privacySensitive(protectScreen)
ChatListView(activeUserPickerSheet: $chatListUserPickerSheet).privacySensitive(protectScreen)
.onAppear {
requestNtfAuthorization()
// Local Authentication notice is to be shown on next start after onboarding is complete
@@ -300,9 +297,18 @@ struct ContentView: View {
if let contactId = contacts?.first?.personHandle?.value,
let chat = chatModel.getChat(contactId),
case let .direct(contact) = chat.chatInfo {
logger.debug("callToRecentContact: schedule call")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
CallController.shared.startCall(contact, mediaType)
let activeCall = chatModel.activeCall
// This line works when a user clicks on a video button in CallKit UI while in call.
// The app tries to make another call to the same contact and overwite activeCall instance making its state broken
if let activeCall, contactId == activeCall.contact.id, mediaType == .video, !activeCall.hasVideo {
Task {
await chatModel.callCommand.processCommand(.media(source: .camera, enable: true))
}
} else if activeCall == nil {
logger.debug("callToRecentContact: schedule call")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
CallController.shared.startCall(contact, mediaType)
}
}
}
}
+38 -80
View File
@@ -61,7 +61,7 @@ class ItemsModel: ObservableObject {
init() {
publisher
.throttle(for: 0.25, scheduler: DispatchQueue.main, latest: true)
.throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true)
.sink { self.objectWillChange.send() }
.store(in: &bag)
}
@@ -123,6 +123,14 @@ class NetworkModel: ObservableObject {
}
}
/// ChatItemWithMenu can depend on previous or next item for it's appearance
/// This dummy model is used to force an update of all chat items,
/// when they might have changed appearance.
class ChatItemDummyModel: ObservableObject {
static let shared = ChatItemDummyModel()
func sendUpdate() { objectWillChange.send() }
}
final class ChatModel: ObservableObject {
@Published var onboardingStage: OnboardingStage?
@Published var setDeliveryReceipts = false
@@ -143,7 +151,7 @@ final class ChatModel: ObservableObject {
@Published var contentViewAccessAuthenticated: Bool = false
@Published var laRequest: LocalAuthRequest?
// list of chat "previews"
@Published var chats: [Chat] = []
@Published private(set) var chats: [Chat] = []
@Published var deletedChats: Set<String> = []
// current chat
@Published var chatId: String?
@@ -183,7 +191,6 @@ final class ChatModel: ObservableObject {
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
@Published var draft: ComposeState?
@Published var draftChatId: String?
@Published var pasteboardHasStrings: Bool = UIPasteboard.general.hasStrings
@Published var networkInfo = UserNetworkInfo(networkType: .other, online: true)
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
@@ -357,25 +364,8 @@ final class ChatModel: ObservableObject {
}
}
func updateChats(with newChats: [ChatData]) {
for i in 0..<newChats.count {
let c = newChats[i]
if let j = getChatIndex(c.id) {
let chat = chats[j]
chat.chatInfo = c.chatInfo
chat.chatItems = c.chatItems
chat.chatStats = c.chatStats
if i != j {
if chatId != c.chatInfo.id {
popChat_(j, to: i)
} else if i == 0 {
chatToTop = c.chatInfo.id
}
}
} else {
addChat_(Chat(c), at: i)
}
}
func updateChats(_ newChats: [ChatData]) {
chats = newChats.map { Chat($0) }
NtfManager.shared.setNtfBadgeCount(totalUnreadCountForAllUsers())
popChatCollector.clear()
}
@@ -445,19 +435,17 @@ final class ChatModel: ObservableObject {
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
if let i = getChatItemIndex(cItem) {
withConditionalAnimation {
_updateChatItem(at: i, with: cItem)
}
_updateChatItem(at: i, with: cItem)
ChatItemDummyModel.shared.sendUpdate()
return false
} else {
withConditionalAnimation(itemAnimation()) {
var ci = cItem
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
ci.meta.itemStatus = status
}
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
im.itemAdded = true
var ci = cItem
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
ci.meta.itemStatus = status
}
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
im.itemAdded = true
ChatItemDummyModel.shared.sendUpdate()
return true
}
@@ -538,7 +526,7 @@ final class ChatModel: ObservableObject {
}
func updateCurrentUserUiThemes(uiThemes: ThemeModeOverrides?) {
guard var current = currentUser else { return }
guard var current = currentUser, current.uiThemes != uiThemes else { return }
current.uiThemes = uiThemes
let i = users.firstIndex(where: { $0.user.userId == current.userId })
if let i {
@@ -574,6 +562,7 @@ final class ChatModel: ObservableObject {
// update preview
_updateChat(cInfo.id) { chat in
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
self.updateFloatingButtons(unreadCount: 0)
chat.chatStats = ChatStats()
}
// update current chat
@@ -590,6 +579,12 @@ final class ChatModel: ObservableObject {
}
}
private func updateFloatingButtons(unreadCount: Int) {
let fbm = ChatView.FloatingButtonModel.shared
fbm.totalUnread = unreadCount
fbm.objectWillChange.send()
}
func markChatItemsRead(_ cInfo: ChatInfo, aboveItem: ChatItem? = nil) {
if let cItem = aboveItem {
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
@@ -608,6 +603,7 @@ final class ChatModel: ObservableObject {
if markedCount > 0 {
chat.chatStats.unreadCount -= markedCount
self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount)
self.updateFloatingButtons(unreadCount: chat.chatStats.unreadCount)
}
}
}
@@ -637,19 +633,15 @@ final class ChatModel: ObservableObject {
}
}
func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
if chatId == cInfo.id,
let itemIndex = getChatItemIndex(cItem),
im.reversedChatItems[itemIndex].isRcvNew {
await MainActor.run {
withTransaction(Transaction()) {
// update current chat
markChatItemRead_(itemIndex)
// update preview
unreadCollector.changeUnreadCounter(cInfo.id, by: -1)
func markChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) {
if self.chatId == cInfo.id {
for itemId in itemIds {
if let i = im.reversedChatItems.firstIndex(where: { $0.id == itemId }) {
markChatItemRead_(i)
}
}
}
self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count)
}
private let unreadCollector = UnreadCollector()
@@ -675,9 +667,10 @@ final class ChatModel: ObservableObject {
}
func changeUnreadCounter(_ chatId: ChatId, by count: Int) {
DispatchQueue.main.async {
self.unreadCounts[chatId] = (self.unreadCounts[chatId] ?? 0) + count
if chatId == ChatModel.shared.chatId {
ChatView.FloatingButtonModel.shared.totalUnread += count
}
self.unreadCounts[chatId] = (self.unreadCounts[chatId] ?? 0) + count
subject.send()
}
}
@@ -899,35 +892,6 @@ final class ChatModel: ObservableObject {
_ = upsertGroupMember(groupInfo, updatedMember)
}
}
func unreadChatItemCounts(itemsInView: Set<String>) -> UnreadChatItemCounts {
var i = 0
var totalBelow = 0
var unreadBelow = 0
while i < im.reversedChatItems.count - 1 && !itemsInView.contains(im.reversedChatItems[i].viewId) {
totalBelow += 1
if im.reversedChatItems[i].isRcvNew {
unreadBelow += 1
}
i += 1
}
return UnreadChatItemCounts(
// TODO these thresholds account for the fact that items are still "visible" while
// covered by compose area, they should be replaced with the actual height in pixels below the screen.
isNearBottom: totalBelow < 15,
isReallyNearBottom: totalBelow < 2,
unreadBelow: unreadBelow
)
}
func topItemInView(itemsInView: Set<String>) -> ChatItem? {
let maxIx = im.reversedChatItems.count - 1
var i = 0
let inView = { itemsInView.contains(self.im.reversedChatItems[$0].viewId) }
while i < maxIx && !inView(i) { i += 1 }
while i < maxIx && inView(i) { i += 1 }
return im.reversedChatItems[min(i - 1, maxIx)]
}
}
struct ShowingInvitation {
@@ -940,12 +904,6 @@ struct NTFContactRequest {
var chatId: String
}
struct UnreadChatItemCounts: Equatable {
var isNearBottom: Bool
var isReallyNearBottom: Bool
var unreadBelow: Int
}
final class Chat: ObservableObject, Identifiable, ChatLike {
@Published var chatInfo: ChatInfo
@Published var chatItems: [ChatItem]
+236 -132
View File
@@ -112,9 +112,9 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
return resp
}
func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil) async -> ChatResponse {
func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil, log: Bool = true) async -> ChatResponse {
await withCheckedContinuation { cont in
cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl))
cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl, log: log))
}
}
@@ -357,17 +357,23 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
throw r
}
func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?) async -> ChatItem? {
let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId, ttl: ttl)
func apiPlanForwardChatItems(type: ChatType, id: Int64, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) {
let r = await chatSendCmd(.apiPlanForwardChatItems(toChatType: type, toChatId: id, itemIds: itemIds))
if case let .forwardPlan(_, chatItemIds, forwardConfimation) = r { return (chatItemIds, forwardConfimation) }
throw r
}
func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? {
let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemIds: itemIds, ttl: ttl)
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
}
func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? {
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
func apiSendMessages(type: ChatType, id: Int64, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
let cmd: ChatCommand = .apiSendMessages(type: type, id: id, live: live, ttl: ttl, composedMessages: composedMessages)
return await processSendMessageCmd(toChatType: type, cmd: cmd)
}
private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> ChatItem? {
private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> [ChatItem]? {
let chatModel = ChatModel.shared
let r: ChatResponse
if toChatType == .direct {
@@ -380,10 +386,13 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async
}
})
r = await chatSendCmd(cmd, bgTask: false)
if case let .newChatItem(_, aChatItem) = r {
cItem = aChatItem.chatItem
chatModel.messageDelivery[aChatItem.chatItem.id] = endTask
return cItem
if case let .newChatItems(_, aChatItems) = r {
let cItems = aChatItems.map { $0.chatItem }
if let cItemLast = cItems.last {
cItem = cItemLast
chatModel.messageDelivery[cItemLast.id] = endTask
}
return cItems
}
if let networkErrorAlert = networkErrorAlert(r) {
AlertManager.shared.showAlert(networkErrorAlert)
@@ -394,18 +403,18 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async
return nil
} else {
r = await chatSendCmd(cmd, bgDelay: msgDelay)
if case let .newChatItem(_, aChatItem) = r {
return aChatItem.chatItem
if case let .newChatItems(_, aChatItems) = r {
return aChatItems.map { $0.chatItem }
}
sendMessageErrorAlert(r)
return nil
}
}
func apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) async -> ChatItem? {
let r = await chatSendCmd(.apiCreateChatItem(noteFolderId: noteFolderId, file: file, msg: msg))
if case let .newChatItem(_, aChatItem) = r { return aChatItem.chatItem }
createChatItemErrorAlert(r)
func apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
let r = await chatSendCmd(.apiCreateChatItems(noteFolderId: noteFolderId, composedMessages: composedMessages))
if case let .newChatItems(_, aChatItems) = r { return aChatItems.map { $0.chatItem } }
createChatItemsErrorAlert(r)
return nil
}
@@ -417,8 +426,8 @@ private func sendMessageErrorAlert(_ r: ChatResponse) {
)
}
private func createChatItemErrorAlert(_ r: ChatResponse) {
logger.error("apiCreateChatItem error: \(String(describing: r))")
private func createChatItemsErrorAlert(_ r: ChatResponse) {
logger.error("apiCreateChatItems error: \(String(describing: r))")
AlertManager.shared.showAlertMsg(
title: "Error creating message",
message: "Error: \(responseError(r))"
@@ -453,7 +462,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String
let r = chatSendCmdSync(.apiGetNtfToken)
switch r {
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:
logger.debug("apiGetNtfToken response: \(String(describing: r))")
return (nil, nil, .off, nil)
@@ -576,19 +585,25 @@ func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profi
throw r
}
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, ConnectionStats?) {
func apiGroupMemberInfoSync(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, ConnectionStats?) {
let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberInfo(_, _, member, connStats_) = r { return (member, connStats_) }
throw r
}
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (GroupMember, ConnectionStats?) {
let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberInfo(_, _, member, connStats_) = r { return (member, connStats_) }
throw r
}
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) {
let r = await chatSendCmd(.apiContactQueueInfo(contactId: contactId))
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
throw r
}
func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) {
let r = await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
throw r
@@ -636,8 +651,8 @@ func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) {
throw r
}
func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, String) {
let r = chatSendCmdSync(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId))
func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (GroupMember, String) {
let r = await chatSendCmd(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberCode(_, _, member, connectionCode) = r { return (member, connectionCode) }
throw r
}
@@ -673,6 +688,13 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P
throw r
}
func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection? {
let r = await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId))
if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection}
throw r
}
func apiConnectPlan(connReq: String) async throws -> ConnectionPlan {
let userId = try currentUserId("apiConnectPlan")
let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq))
@@ -990,6 +1012,10 @@ func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async thr
try await sendCommandOkResp(.apiChatRead(type: type, id: id, itemRange: itemRange))
}
func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws {
try await sendCommandOkResp(.apiChatItemsRead(type: type, id: id, itemIds: itemIds))
}
func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
}
@@ -1025,77 +1051,122 @@ func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationF
}
func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async {
if let chatItem = await apiReceiveFile(
fileId: fileId,
userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
encrypted: privacyEncryptLocalFilesGroupDefault.get(),
await receiveFiles(
user: user,
fileIds: [fileId],
userApprovedRelays: userApprovedRelays,
auto: auto
) {
await chatItemSimpleUpdate(user, chatItem)
}
)
}
func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
let r = await chatSendCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
let am = AlertManager.shared
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
if case .rcvFileAcceptedSndCancelled = r {
logger.debug("apiReceiveFile error: sender cancelled file transfer")
if !auto {
am.showAlertMsg(
title: "Cannot receive file",
message: "Sender cancelled file transfer."
func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool = false, auto: Bool = false) async {
var fileIdsToApprove = [Int64]()
var srvsToApprove = Set<String>()
var otherFileErrs = [ChatResponse]()
for fileId in fileIds {
let r = await chatSendCmd(
.receiveFile(
fileId: fileId,
userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
encrypted: privacyEncryptLocalFilesGroupDefault.get(),
inline: nil
)
)
switch r {
case let .rcvFileAccepted(_, chatItem):
await chatItemSimpleUpdate(user, chatItem)
default:
if let chatError = chatError(r) {
switch chatError {
case let .fileNotApproved(fileId, unknownServers):
fileIdsToApprove.append(fileId)
srvsToApprove.formUnion(unknownServers)
default:
otherFileErrs.append(r)
}
}
}
} else if let networkErrorAlert = networkErrorAlert(r) {
logger.error("apiReceiveFile network error: \(String(describing: r))")
if !auto {
am.showAlert(networkErrorAlert)
}
if !auto {
let otherErrsStr = if otherFileErrs.isEmpty {
""
} else if otherFileErrs.count == 1 {
"\(otherFileErrs[0])"
} else if otherFileErrs.count == 2 {
"\(otherFileErrs[0])\n\(otherFileErrs[1])"
} else {
"\(otherFileErrs[0])\n\(otherFileErrs[1])\nand \(otherFileErrs.count - 2) other error(s)"
}
} else {
switch chatError(r) {
case .fileCancelled:
logger.debug("apiReceiveFile ignoring fileCancelled error")
case .fileAlreadyReceiving:
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
case let .fileNotApproved(fileId, unknownServers):
logger.debug("apiReceiveFile fileNotApproved error")
if !auto {
let srvs = unknownServers.map { s in
// If there are not approved files, alert is shown the same way both in case of singular and plural files reception
if !fileIdsToApprove.isEmpty {
let srvs = srvsToApprove
.map { s in
if let srv = parseServerAddress(s), !srv.hostnames.isEmpty {
srv.hostnames[0]
} else {
serverHost(s)
}
}
am.showAlert(Alert(
title: Text("Unknown servers!"),
message: Text("Without Tor or VPN, your IP address will be visible to these XFTP relays: \(srvs.sorted().joined(separator: ", "))."),
primaryButton: .default(
Text("Download"),
action: {
Task {
logger.debug("apiReceiveFile fileNotApproved alert - in Task")
if let user = ChatModel.shared.currentUser {
await receiveFile(user: user, fileId: fileId, userApprovedRelays: true)
}
.sorted()
.joined(separator: ", ")
let fIds = fileIdsToApprove
await MainActor.run {
showAlert(
title: NSLocalizedString("Unknown servers!", comment: "alert title"),
message: (
String.localizedStringWithFormat(NSLocalizedString("Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.", comment: "alert message"), srvs) +
(otherErrsStr != "" ? "\n\n" + String.localizedStringWithFormat(NSLocalizedString("Other file errors:\n%@", comment: "alert message"), otherErrsStr) : "")
),
buttonTitle: NSLocalizedString("Download", comment: "alert button"),
buttonAction: {
Task {
logger.debug("apiReceiveFile fileNotApproved alert - in Task")
if let user = ChatModel.shared.currentUser {
await receiveFiles(user: user, fileIds: fIds, userApprovedRelays: true)
}
}
),
secondaryButton: .cancel()
))
},
cancelButton: true
)
}
default:
logger.error("apiReceiveFile error: \(String(describing: r))")
if !auto {
am.showAlertMsg(
title: "Error receiving file",
message: "Error: \(responseError(r))"
} else if otherFileErrs.count == 1 { // If there is a single other error, we differentiate on it
let errorResponse = otherFileErrs.first!
switch errorResponse {
case let .rcvFileAcceptedSndCancelled(_, rcvFileTransfer):
logger.debug("receiveFiles error: sender cancelled file transfer \(rcvFileTransfer.fileId)")
await MainActor.run {
showAlert(
NSLocalizedString("Cannot receive file", comment: "alert title"),
message: NSLocalizedString("Sender cancelled file transfer.", comment: "alert message")
)
}
default:
if let chatError = chatError(errorResponse) {
switch chatError {
case .fileCancelled, .fileAlreadyReceiving:
logger.debug("receiveFiles ignoring FileCancelled or FileAlreadyReceiving error")
default:
await MainActor.run {
showAlert(
NSLocalizedString("Error receiving file", comment: "alert title"),
message: responseError(errorResponse)
)
}
}
}
}
} else if otherFileErrs.count > 1 { // If there are multiple other errors, we show general alert
await MainActor.run {
showAlert(
NSLocalizedString("Error receiving file", comment: "alert title"),
message: String.localizedStringWithFormat(NSLocalizedString("File errors:\n%@", comment: "alert message"), otherErrsStr)
)
}
}
}
return nil
}
func cancelFile(user: User, fileId: Int64) async {
@@ -1218,12 +1289,18 @@ func apiEndCall(_ contact: Contact) async throws {
try await sendCommandOkResp(.apiEndCall(contact: contact))
}
func apiGetCallInvitations() throws -> [RcvCallInvitation] {
func apiGetCallInvitationsSync() throws -> [RcvCallInvitation] {
let r = chatSendCmdSync(.apiGetCallInvitations)
if case let .callInvitations(invs) = r { return invs }
throw r
}
func apiGetCallInvitations() async throws -> [RcvCallInvitation] {
let r = await chatSendCmd(.apiGetCallInvitations)
if case let .callInvitations(invs) = r { return invs }
throw r
}
func apiCallStatus(_ contact: Contact, _ status: String) async throws {
if let callStatus = WebRTCCallStatus.init(rawValue: status) {
try await sendCommandOkResp(.apiCallStatus(contact: contact, callStatus: callStatus))
@@ -1271,11 +1348,23 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async {
func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
do {
logger.debug("apiMarkChatItemRead: \(cItem.id)")
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id))
await ChatModel.shared.markChatItemRead(cInfo, cItem)
DispatchQueue.main.async {
ChatModel.shared.markChatItemsRead(cInfo, [cItem.id])
}
} catch {
logger.error("apiMarkChatItemRead apiChatRead error: \(responseError(error))")
logger.error("apiChatRead error: \(responseError(error))")
}
}
func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) async {
do {
try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds)
DispatchQueue.main.async {
ChatModel.shared.markChatItemsRead(cInfo, itemIds)
}
} catch {
logger.error("apiChatItemsRead error: \(responseError(error))")
}
}
@@ -1422,7 +1511,7 @@ func apiGetVersion() throws -> CoreVersionInfo {
func getAgentSubsTotal() async throws -> (SMPServerSubs, Bool) {
let userId = try currentUserId("getAgentSubsTotal")
let r = await chatSendCmd(.getAgentSubsTotal(userId: userId))
let r = await chatSendCmd(.getAgentSubsTotal(userId: userId), log: false)
if case let .agentSubsTotal(_, subsTotal, hasSession) = r { return (subsTotal, hasSession) }
logger.error("getAgentSubsTotal error: \(String(describing: r))")
throw r
@@ -1517,7 +1606,7 @@ func startChat(refreshInvitations: Bool = true) throws {
try getUserChatData()
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers())
if (refreshInvitations) {
try refreshCallInvitations()
Task { try await refreshCallInvitations() }
}
(m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken()
_ = try apiStartChat()
@@ -1591,8 +1680,7 @@ func getUserChatData() throws {
m.userAddress = try apiGetUserAddress()
m.chatItemTTL = try getChatItemTTL()
let chats = try apiGetChats()
m.chats = chats.map { Chat.init($0) }
m.popChatCollector.clear()
m.updateChats(chats)
}
private func getUserChatDataAsync() async throws {
@@ -1604,14 +1692,12 @@ private func getUserChatDataAsync() async throws {
await MainActor.run {
m.userAddress = userAddress
m.chatItemTTL = chatItemTTL
m.chats = chats.map { Chat.init($0) }
m.popChatCollector.clear()
m.updateChats(chats)
}
} else {
await MainActor.run {
m.userAddress = nil
m.chats = []
m.popChatCollector.clear()
m.updateChats([])
}
}
}
@@ -1772,36 +1858,45 @@ func processReceivedMsg(_ res: ChatResponse) async {
n.networkStatuses = ns
}
}
case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
await MainActor.run {
if active(user) {
m.addChatItem(cInfo, cItem)
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
m.increaseUnreadCounter(user: user)
case let .newChatItems(user, chatItems):
for chatItem in chatItems {
let cInfo = chatItem.chatInfo
let cItem = chatItem.chatItem
await MainActor.run {
if active(user) {
m.addChatItem(cInfo, cItem)
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
m.increaseUnreadCounter(user: user)
}
}
if let file = cItem.autoReceiveFile() {
Task {
await receiveFile(user: user, fileId: file.fileId, auto: true)
}
}
if cItem.showNotification {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
}
if let file = cItem.autoReceiveFile() {
Task {
await receiveFile(user: user, fileId: file.fileId, auto: true)
case let .chatItemsStatusesUpdated(user, chatItems):
for chatItem in chatItems {
let cInfo = chatItem.chatInfo
let cItem = chatItem.chatItem
if !cItem.isDeletedContent && active(user) {
await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) }
}
}
if cItem.showNotification {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
case let .chatItemStatusUpdated(user, aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
if !cItem.isDeletedContent && active(user) {
await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) }
}
if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
case .sndSent: endTask()
case .sndErrorAuth: endTask()
case .sndError: endTask()
default: ()
if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
case .sndNew: ()
case .sndSent: endTask()
case .sndRcvd: endTask()
case .sndErrorAuth: endTask()
case .sndError: endTask()
case .sndWarning: endTask()
case .rcvNew: ()
case .rcvRead: ()
case .invalid: ()
}
}
}
case let .chatItemUpdated(user, aChatItem):
@@ -1995,7 +2090,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
await withCall(contact) { call in
await MainActor.run {
call.callState = .offerReceived
call.peerMedia = callType.media
call.sharedKey = sharedKey
}
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
@@ -2164,32 +2258,42 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async {
}
}
func refreshCallInvitations() throws {
func refreshCallInvitations() async throws {
let m = ChatModel.shared
let callInvitations = try justRefreshCallInvitations()
if let (chatId, ntfAction) = m.ntfCallInvitationAction,
let invitation = m.callInvitations.removeValue(forKey: chatId) {
m.ntfCallInvitationAction = nil
CallController.shared.callAction(invitation: invitation, action: ntfAction)
} else if let invitation = callInvitations.last(where: { $0.user.showNotifications }) {
activateCall(invitation)
let callInvitations = try await apiGetCallInvitations()
await MainActor.run {
m.callInvitations = callsByChat(callInvitations)
if let (chatId, ntfAction) = m.ntfCallInvitationAction,
let invitation = m.callInvitations.removeValue(forKey: chatId) {
m.ntfCallInvitationAction = nil
CallController.shared.callAction(invitation: invitation, action: ntfAction)
} else if let invitation = callInvitations.last(where: { $0.user.showNotifications }) {
activateCall(invitation)
}
}
}
func justRefreshCallInvitations() throws -> [RcvCallInvitation] {
let m = ChatModel.shared
let callInvitations = try apiGetCallInvitations()
m.callInvitations = callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { result, inv in result[inv.contact.id] = inv }
return callInvitations
func justRefreshCallInvitations() async throws {
let callInvitations = try apiGetCallInvitationsSync()
await MainActor.run {
ChatModel.shared.callInvitations = callsByChat(callInvitations)
}
}
private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: RcvCallInvitation] {
callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) {
result, inv in result[inv.contact.id] = inv
}
}
func activateCall(_ callInvitation: RcvCallInvitation) {
if !callInvitation.user.showNotifications { return }
let m = ChatModel.shared
logger.debug("reportNewIncomingCall activeCallUUID \(String(describing: m.activeCall?.callUUID)) invitationUUID \(String(describing: callInvitation.callUUID))")
if !callInvitation.user.showNotifications || m.activeCall?.callUUID == callInvitation.callUUID { return }
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
if let error = error {
DispatchQueue.main.async {
m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil
m.callInvitations[callInvitation.contact.id]?.callUUID = nil
}
logger.error("reportNewIncomingCall error: \(error.localizedDescription)")
} else {
+11 -9
View File
@@ -83,9 +83,11 @@ struct SimpleXApp: App {
if appState != .stopped {
startChatAndActivate {
if appState.inactive && chatModel.chatRunning == true {
updateChats()
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
updateCallInvitations()
Task {
await updateChats()
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
await updateCallInvitations()
}
}
}
}
@@ -130,16 +132,16 @@ struct SimpleXApp: App {
}
}
private func updateChats() {
private func updateChats() async {
do {
let chats = try apiGetChats()
chatModel.updateChats(with: chats)
let chats = try await apiGetChatsAsync()
await MainActor.run { chatModel.updateChats(chats) }
if let id = chatModel.chatId,
let chat = chatModel.getChat(id) {
Task { await loadChat(chat: chat, clearItems: false) }
}
if let ncr = chatModel.ntfContactRequest {
chatModel.ntfContactRequest = nil
await MainActor.run { chatModel.ntfContactRequest = nil }
if case let .contactRequest(contactRequest) = chatModel.getChat(ncr.chatId)?.chatInfo {
Task { await acceptContactRequest(incognito: ncr.incognito, contactRequest: contactRequest) }
}
@@ -149,9 +151,9 @@ struct SimpleXApp: App {
}
}
private func updateCallInvitations() {
private func updateCallInvitations() async {
do {
try refreshCallInvitations()
try await refreshCallInvitations()
} catch let error {
logger.error("apiGetCallInvitations: cannot update call invitations \(responseError(error))")
}
+4 -4
View File
@@ -91,8 +91,8 @@ var systemInDarkThemeCurrently: Bool {
return UITraitCollection.current.userInterfaceStyle == .dark
}
func reactOnDarkThemeChanges() {
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.colors.isLight == systemInDarkThemeCurrently {
func reactOnDarkThemeChanges(_ inDarkNow: Bool) {
if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.colors.isLight == inDarkNow {
// Change active colors from light to dark and back based on system theme
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
}
@@ -102,7 +102,7 @@ extension ThemeWallpaper {
public func importFromString() -> ThemeWallpaper {
if preset == nil, let image {
// Need to save image from string and to save its path
if let parsed = UIImage(base64Encoded: image),
if let parsed = imageFromBase64(image),
let filename = saveWallpaperFile(image: parsed) {
var copy = self
copy.image = nil
@@ -122,7 +122,7 @@ extension ThemeWallpaper {
let preset: String? = if case let WallpaperType.preset(filename, _) = type { filename } else { nil }
let scale: Float? = if case let WallpaperType.preset(_, scale) = type { scale } else { if case let WallpaperType.image(_, scale, _) = type { scale } else { 1.0 } }
let scaleType: WallpaperScaleType? = if case let WallpaperType.image(_, _, scaleType) = type { scaleType } else { nil }
let image: String? = if case WallpaperType.image = type, let image = type.uiImage { resizeImageToStrSize(image, maxDataSize: 5_000_000) } else { nil }
let image: String? = if case WallpaperType.image = type, let image = type.uiImage { resizeImageToStrSizeSync(image, maxDataSize: 5_000_000) } else { nil }
return ThemeWallpaper (
preset: preset,
scale: scale,
+2 -2
View File
@@ -53,7 +53,7 @@ class ThemeManager {
return perUserTheme
}
let defaultTheme = defaultActiveTheme(appSettingsTheme)
return ThemeModeOverride(mode: CurrentColors.base.mode, colors: defaultTheme?.colors ?? ThemeColors(), wallpaper: defaultTheme?.wallpaper)
return ThemeModeOverride(mode: CurrentColors.base.mode, colors: defaultTheme?.colors ?? ThemeColors(), wallpaper: defaultTheme?.wallpaper ?? ThemeWallpaper.from(PresetWallpaper.school.toType(CurrentColors.base), nil, nil))
}
static func currentColors(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ActiveTheme {
@@ -197,7 +197,7 @@ class ThemeManager {
var themeIds = currentThemeIdsDefault.get()
themeIds[nonSystemThemeName] = prevValue.themeId
currentThemeIdsDefault.set(themeIds)
applyTheme(nonSystemThemeName)
applyTheme(currentThemeDefault.get())
}
static func copyFromSameThemeOverrides(_ type: WallpaperType?, _ lowerLevelOverride: ThemeModeOverride?, _ pref: Binding<ThemeModeOverride>) -> Bool {
+167 -115
View File
@@ -17,8 +17,8 @@ struct ActiveCallView: View {
@ObservedObject var call: Call
@Environment(\.scenePhase) var scenePhase
@State private var client: WebRTCClient? = nil
@State private var activeCall: WebRTCClient.Call? = nil
@State private var localRendererAspectRatio: CGFloat? = nil
@State var remoteContentMode: UIView.ContentMode = .scaleAspectFill
@Binding var canConnectCall: Bool
@State var prevColorScheme: ColorScheme = .dark
@State var pipShown = false
@@ -27,24 +27,39 @@ struct ActiveCallView: View {
var body: some View {
ZStack(alignment: .topLeading) {
ZStack(alignment: .bottom) {
if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil {
if let client = client, call.hasVideo {
GeometryReader { g in
let width = g.size.width * 0.3
ZStack(alignment: .topTrailing) {
CallViewRemote(client: client, activeCall: $activeCall, activeCallViewIsCollapsed: $m.activeCallViewIsCollapsed, pipShown: $pipShown)
CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio, pipShown: $pipShown)
.cornerRadius(10)
.frame(width: width, height: width / (localRendererAspectRatio ?? 1))
.padding([.top, .trailing], 17)
ZStack(alignment: .center) {
// For some reason, when the view in GeometryReader and ZStack is visible, it steals clicks on a back button, so showing something on top like this with background color helps (.clear color doesn't work)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.primary.opacity(0.000001))
CallViewRemote(client: client, call: call, activeCallViewIsCollapsed: $m.activeCallViewIsCollapsed, contentMode: $remoteContentMode, pipShown: $pipShown)
.onTapGesture {
remoteContentMode = remoteContentMode == .scaleAspectFill ? .scaleAspectFit : .scaleAspectFill
}
Group {
let localVideoTrack = client.activeCall?.localVideoTrack ?? client.notConnectedCall?.localCameraAndTrack?.1
if localVideoTrack != nil {
CallViewLocal(client: client, localRendererAspectRatio: $localRendererAspectRatio, pipShown: $pipShown)
.onDisappear {
localRendererAspectRatio = nil
}
} else {
Rectangle().fill(.black)
}
}
.cornerRadius(10)
.frame(width: width, height: localRendererAspectRatio == nil ? (g.size.width < g.size.height ? width * 1.33 : width / 1.33) : width / (localRendererAspectRatio ?? 1))
.padding([.top, .trailing], 17)
}
}
}
if let call = m.activeCall, let client = client, (!pipShown || !call.supportsVideo) {
if let call = m.activeCall, let client = client, (!pipShown || !call.hasVideo) {
ActiveCallOverlay(call: call, client: client)
}
}
@@ -54,6 +69,9 @@ struct ActiveCallView: View {
.onAppear {
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)")
AppDelegate.keepScreenOn(true)
Task {
await askRequiredPermissions()
}
createWebRTCClient()
dismissAllSheets()
hideKeyboard()
@@ -84,7 +102,7 @@ struct ActiveCallView: View {
private func createWebRTCClient() {
if client == nil && canConnectCall {
client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
client = WebRTCClient({ msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
Task {
await m.callCommand.setClient(client)
}
@@ -99,7 +117,7 @@ struct ActiveCallView: View {
logger.debug("ActiveCallView: response \(msg.resp.respType)")
switch msg.resp {
case let .capabilities(capabilities):
let callType = CallType(media: call.localMedia, capabilities: capabilities)
let callType = CallType(media: call.initialCallType, capabilities: capabilities)
Task {
do {
try await apiSendCallInvitation(call.contact, callType)
@@ -110,7 +128,7 @@ struct ActiveCallView: View {
call.callState = .invitationSent
call.localCapabilities = capabilities
}
if call.supportsVideo && !AVAudioSession.sharedInstance().hasExternalAudioDevice() {
if call.hasVideo && !AVAudioSession.sharedInstance().hasExternalAudioDevice() {
try? AVAudioSession.sharedInstance().setCategory(.playback, options: [.allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
}
CallSoundsPlayer.shared.startConnectingCallSound()
@@ -120,7 +138,7 @@ struct ActiveCallView: View {
Task {
do {
try await apiSendCallOffer(call.contact, offer, iceCandidates,
media: call.localMedia, capabilities: capabilities)
media: call.initialCallType, capabilities: capabilities)
} catch {
logger.error("apiSendCallOffer \(responseError(error))")
}
@@ -164,6 +182,9 @@ struct ActiveCallView: View {
}
if state.connectionState == "closed" {
closeCallView(client)
if let callUUID = m.activeCall?.callUUID {
CallController.shared.endCall(callUUID: callUUID)
}
m.activeCall = nil
m.activeCallViewIsCollapsed = false
}
@@ -182,10 +203,18 @@ struct ActiveCallView: View {
CallSoundsPlayer.shared.vibrate(long: false)
wasConnected = true
}
case let .peerMedia(source, enabled):
switch source {
case .mic: call.peerMediaSources.mic = enabled
case .camera: call.peerMediaSources.camera = enabled
case .screenAudio: call.peerMediaSources.screenAudio = enabled
case .screenVideo: call.peerMediaSources.screenVideo = enabled
case .unknown: ()
}
case .ended:
closeCallView(client)
call.callState = .ended
if let uuid = call.callkitUUID {
if let uuid = call.callUUID {
CallController.shared.endCall(callUUID: uuid)
}
case .ok:
@@ -214,16 +243,38 @@ struct ActiveCallView: View {
ChatReceiver.shared.messagesChannel = nil
return
}
if case let .chatItemStatusUpdated(_, msg) = msg,
msg.chatInfo.id == call.contact.id,
case .sndCall = msg.chatItem.content,
case .sndRcvd = msg.chatItem.meta.itemStatus {
if case let .chatItemsStatusesUpdated(_, chatItems) = msg,
chatItems.contains(where: { ci in
ci.chatInfo.id == call.contact.id &&
ci.chatItem.content.isSndCall &&
ci.chatItem.meta.itemStatus.isSndRcvd
}) {
CallSoundsPlayer.shared.startInCallSound()
ChatReceiver.shared.messagesChannel = nil
}
}
}
private func askRequiredPermissions() async {
let mic = await WebRTCClient.isAuthorized(for: .audio)
await MainActor.run {
call.localMediaSources.mic = mic
}
let cameraAuthorized = AVCaptureDevice.authorizationStatus(for: .video) == .authorized
var camera = call.initialCallType == .audio || cameraAuthorized
if call.initialCallType == .video && !cameraAuthorized {
camera = await WebRTCClient.isAuthorized(for: .video)
await MainActor.run {
if camera, let client {
client.setCameraEnabled(true)
}
}
}
if !mic || !camera {
WebRTCClient.showUnauthorizedAlert(for: !mic ? .audio : .video)
}
}
private func closeCallView(_ client: WebRTCClient) {
if m.activeCall != nil {
m.showCallView = false
@@ -239,44 +290,16 @@ struct ActiveCallOverlay: View {
var body: some View {
VStack {
switch call.localMedia {
case .video:
switch call.hasVideo {
case true:
videoCallInfoView(call)
.foregroundColor(.white)
.opacity(0.8)
.padding()
Spacer()
HStack {
toggleAudioButton()
Spacer()
if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) {
toggleSpeakerButton()
.frame(width: 40, height: 40)
} else if call.hasMedia {
AudioDevicePicker()
.scaleEffect(2)
.frame(maxWidth: 40, maxHeight: 40)
} else {
Color.clear.frame(width: 40, height: 40)
}
Spacer()
endCallButton()
Spacer()
if call.videoEnabled {
flipCameraButton()
} else {
Color.clear.frame(width: 40, height: 40)
}
Spacer()
toggleVideoButton()
}
.padding(.horizontal, 20)
.padding(.bottom, 16)
.frame(maxWidth: .infinity, alignment: .center)
case .audio:
.padding(.horizontal)
// Fixed vertical padding required for preserving position of buttons row when changing audio-to-video and back in landscape orientation.
// Otherwise, bigger padding is added by SwiftUI when switching call types
.padding(.vertical, 10)
case false:
ZStack(alignment: .topLeading) {
Button {
chatModel.activeCallViewIsCollapsed = true
@@ -291,35 +314,32 @@ struct ActiveCallOverlay: View {
}
.foregroundColor(.white)
.opacity(0.8)
.padding()
.padding(.horizontal)
.padding(.vertical, 10)
.frame(maxHeight: .infinity)
}
Spacer()
ZStack(alignment: .bottom) {
toggleAudioButton()
.frame(maxWidth: .infinity, alignment: .leading)
endCallButton()
// Check if the only input is microphone. And in this case show toggle button,
// If there are more inputs, it probably means something like bluetooth headphones are available
// and in this case show iOS button for choosing different output.
// There is no way to get available outputs, only inputs
if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) {
toggleSpeakerButton()
.frame(maxWidth: .infinity, alignment: .trailing)
} else if call.hasMedia {
AudioDevicePicker()
.scaleEffect(2)
.frame(maxWidth: 50, maxHeight: 40)
.frame(maxWidth: .infinity, alignment: .trailing)
} else {
Color.clear.frame(width: 50, height: 40)
}
}
.padding(.bottom, 60)
.padding(.horizontal, 48)
}
Spacer()
HStack {
toggleMicButton()
Spacer()
audioDeviceButton()
Spacer()
endCallButton()
Spacer()
if call.localMediaSources.camera {
flipCameraButton()
} else {
Color.clear.frame(width: 60, height: 60)
}
Spacer()
toggleCameraButton()
}
.padding(.horizontal, 20)
.padding(.bottom, 16)
.frame(maxWidth: 440, alignment: .center)
}
.frame(maxWidth: .infinity)
.onAppear {
@@ -381,36 +401,54 @@ struct ActiveCallOverlay: View {
private func endCallButton() -> some View {
let cc = CallController.shared
return callButton("phone.down.fill", width: 60, height: 60) {
if let uuid = call.callkitUUID {
return callButton("phone.down.fill", .red, padding: 10) {
if let uuid = call.callUUID {
cc.endCall(callUUID: uuid)
} else {
cc.endCall(call: call) {}
}
}
.foregroundColor(.red)
}
private func toggleAudioButton() -> some View {
controlButton(call, call.audioEnabled ? "mic.fill" : "mic.slash") {
private func toggleMicButton() -> some View {
controlButton(call, call.localMediaSources.mic ? "mic.fill" : "mic.slash", padding: 14) {
Task {
client.setAudioEnabled(!call.audioEnabled)
DispatchQueue.main.async {
call.audioEnabled = !call.audioEnabled
}
if await WebRTCClient.isAuthorized(for: .audio) {
client.setAudioEnabled(!call.localMediaSources.mic)
} else { WebRTCClient.showUnauthorizedAlert(for: .audio) }
}
}
}
func audioDeviceButton() -> some View {
// Check if the only input is microphone. And in this case show toggle button,
// If there are more inputs, it probably means something like bluetooth headphones are available
// and in this case show iOS button for choosing different output.
// There is no way to get available outputs, only inputs
Group {
if deviceManager.availableInputs.allSatisfy({ $0.portType == .builtInMic }) {
toggleSpeakerButton()
} else {
audioDevicePickerButton()
}
}
.onChange(of: call.localMediaSources.hasVideo) { hasVideo in
let current = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType
let speakerEnabled = current == .builtInSpeaker
let receiverEnabled = current == .builtInReceiver
// react automatically only when receiver were selected, otherwise keep an external device selected
if !speakerEnabled && hasVideo && receiverEnabled {
client.setSpeakerEnabledAndConfigureSession(!speakerEnabled, skipExternalDevice: true)
call.speakerEnabled = !speakerEnabled
}
}
}
private func toggleSpeakerButton() -> some View {
controlButton(call, call.speakerEnabled ? "speaker.wave.2.fill" : "speaker.wave.1.fill") {
Task {
let speakerEnabled = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType == .builtInSpeaker
client.setSpeakerEnabledAndConfigureSession(!speakerEnabled)
DispatchQueue.main.async {
call.speakerEnabled = !speakerEnabled
}
}
controlButton(call, !call.peerMediaSources.mic ? "speaker.slash" : call.speakerEnabled ? "speaker.wave.2.fill" : "speaker.wave.1.fill", padding: !call.peerMediaSources.mic ? 16 : call.speakerEnabled ? 15 : 17) {
let speakerEnabled = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portType == .builtInSpeaker
client.setSpeakerEnabledAndConfigureSession(!speakerEnabled)
call.speakerEnabled = !speakerEnabled
}
.onAppear {
deviceManager.call = call
@@ -418,53 +456,67 @@ struct ActiveCallOverlay: View {
}
}
private func toggleVideoButton() -> some View {
controlButton(call, call.videoEnabled ? "video.fill" : "video.slash") {
private func toggleCameraButton() -> some View {
controlButton(call, call.localMediaSources.camera ? "video.fill" : "video.slash", padding: call.localMediaSources.camera ? 16 : 14) {
Task {
client.setVideoEnabled(!call.videoEnabled)
DispatchQueue.main.async {
call.videoEnabled = !call.videoEnabled
}
if await WebRTCClient.isAuthorized(for: .video) {
client.setCameraEnabled(!call.localMediaSources.camera)
} else { WebRTCClient.showUnauthorizedAlert(for: .video) }
}
}
.disabled(call.initialCallType == .audio && client.activeCall?.peerHasOldVersion == true)
}
@ViewBuilder private func flipCameraButton() -> some View {
controlButton(call, "arrow.triangle.2.circlepath") {
controlButton(call, "arrow.triangle.2.circlepath", padding: 12) {
Task {
client.flipCamera()
if await WebRTCClient.isAuthorized(for: .video) {
client.flipCamera()
}
}
}
}
@ViewBuilder private func controlButton(_ call: Call, _ imageName: String, _ perform: @escaping () -> Void) -> some View {
if call.hasMedia {
callButton(imageName, width: 50, height: 38, perform)
.foregroundColor(.white)
.opacity(0.85)
} else {
Color.clear.frame(width: 50, height: 38)
}
@ViewBuilder private func controlButton(_ call: Call, _ imageName: String, padding: CGFloat, _ perform: @escaping () -> Void) -> some View {
callButton(imageName, call.peerMediaSources.hasVideo ? Color.black.opacity(0.2) : Color.white.opacity(0.2), padding: padding, perform)
}
private func callButton(_ imageName: String, width: CGFloat, height: CGFloat, _ perform: @escaping () -> Void) -> some View {
@ViewBuilder private func audioDevicePickerButton() -> some View {
AudioDevicePicker()
.opacity(0.8)
.scaleEffect(2)
.padding(10)
.frame(width: 60, height: 60)
.background(call.peerMediaSources.hasVideo ? Color.black.opacity(0.2) : Color.white.opacity(0.2))
.clipShape(.circle)
}
private func callButton(_ imageName: String, _ background: Color, padding: CGFloat, _ perform: @escaping () -> Void) -> some View {
Button {
perform()
} label: {
Image(systemName: imageName)
.resizable()
.scaledToFit()
.frame(maxWidth: width, maxHeight: height)
.padding(padding)
.frame(width: 60, height: 60)
.background(background)
}
.foregroundColor(whiteColorWithAlpha)
.clipShape(.circle)
}
private var whiteColorWithAlpha: Color {
get { Color(red: 204 / 255, green: 204 / 255, blue: 204 / 255) }
}
}
struct ActiveCallOverlay_Previews: PreviewProvider {
static var previews: some View {
Group{
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, initialCallType: .video), client: WebRTCClient({ _ in }, Binding.constant(nil)))
.background(.black)
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, initialCallType: .audio), client: WebRTCClient({ _ in }, Binding.constant(nil)))
.background(.black)
}
}
+101 -51
View File
@@ -51,7 +51,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
logger.debug("CallController.provider CXStartCallAction")
if callManager.startOutgoingCall(callUUID: action.callUUID) {
if callManager.startOutgoingCall(callUUID: action.callUUID.uuidString.lowercased()) {
action.fulfill()
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
} else {
@@ -61,12 +61,30 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
logger.debug("CallController.provider CXAnswerCallAction")
if callManager.answerIncomingCall(callUUID: action.callUUID) {
// WebRTC call should be in connected state to fulfill.
// Otherwise no audio and mic working on lockscreen
fulfillOnConnect = action
} else {
action.fail()
Task {
let chatIsReady = await waitUntilChatStarted(timeoutMs: 30_000, stepMs: 500)
logger.debug("CallController chat started \(chatIsReady) \(ChatModel.shared.chatInitialized) \(ChatModel.shared.chatRunning == true) \(String(describing: AppChatState.shared.value))")
if !chatIsReady {
action.fail()
return
}
if !ChatModel.shared.callInvitations.values.contains(where: { inv in inv.callUUID == action.callUUID.uuidString.lowercased() }) {
try? await justRefreshCallInvitations()
logger.debug("CallController: updated call invitations chat")
}
await MainActor.run {
logger.debug("CallController.provider will answer on call")
if callManager.answerIncomingCall(callUUID: action.callUUID.uuidString.lowercased()) {
logger.debug("CallController.provider answered on call")
// WebRTC call should be in connected state to fulfill.
// Otherwise no audio and mic working on lockscreen
fulfillOnConnect = action
} else {
logger.debug("CallController.provider will fail the call")
action.fail()
}
}
}
}
@@ -75,7 +93,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
// Should be nil here if connection was in connected state
fulfillOnConnect?.fail()
fulfillOnConnect = nil
callManager.endCall(callUUID: action.callUUID) { ok in
callManager.endCall(callUUID: action.callUUID.uuidString.lowercased()) { ok in
if ok {
action.fulfill()
} else {
@@ -86,7 +104,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID) {
if callManager.enableMedia(source: .mic, enable: !action.isMuted, callUUID: action.callUUID.uuidString.lowercased()) {
action.fulfill()
} else {
action.fail()
@@ -103,8 +121,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
RTCAudioSession.sharedInstance().isAudioEnabled = true
do {
let supportsVideo = ChatModel.shared.activeCall?.supportsVideo == true
if supportsVideo {
let hasVideo = ChatModel.shared.activeCall?.hasVideo == true
if hasVideo {
try audioSession.setCategory(.playAndRecord, mode: .videoChat, options: [.defaultToSpeaker, .mixWithOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
} else {
try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.mixWithOthers, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
@@ -115,7 +133,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
try? await Task.sleep(nanoseconds: UInt64(i) * 300_000000)
if let preferred = audioSession.preferredInputDevice() {
await MainActor.run { try? audioSession.setPreferredInput(preferred) }
} else if supportsVideo {
} else if hasVideo {
await MainActor.run { try? audioSession.overrideOutputAudioPort(.speaker) }
}
}
@@ -156,6 +174,19 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
}
private func waitUntilChatStarted(timeoutMs: UInt64, stepMs: UInt64) async -> Bool {
logger.debug("CallController waiting until chat started")
var t: UInt64 = 0
repeat {
if ChatModel.shared.chatInitialized, ChatModel.shared.chatRunning == true, case .active = AppChatState.shared.value {
return true
}
_ = try? await Task.sleep(nanoseconds: stepMs * 1000000)
t += stepMs
} while t < timeoutMs
return false
}
@objc(pushRegistry:didUpdatePushCredentials:forType:)
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue)")
@@ -171,32 +202,19 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
self.reportExpiredCall(payload: payload, completion)
return
}
if (!ChatModel.shared.chatInitialized) {
logger.debug("CallController: initializing chat")
do {
try initializeChat(start: true, refreshInvitations: false)
} catch let error {
logger.error("CallController: initializing chat error: \(error)")
self.reportExpiredCall(payload: payload, completion)
return
}
}
logger.debug("CallController: initialized chat")
startChatForCall()
logger.debug("CallController: started chat")
self.shouldSuspendChat = true
// There are no invitations in the model, as it was processed by NSE
_ = try? justRefreshCallInvitations()
logger.debug("CallController: updated call invitations chat")
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
// Extract the call information from the push notification payload
let m = ChatModel.shared
if let contactId = payload.dictionaryPayload["contactId"] as? String,
let invitation = m.callInvitations[contactId] {
let update = self.cxCallUpdate(invitation: invitation)
if let uuid = invitation.callkitUUID {
let displayName = payload.dictionaryPayload["displayName"] as? String,
let callUUID = payload.dictionaryPayload["callUUID"] as? String,
let uuid = UUID(uuidString: callUUID),
let callTsInterval = payload.dictionaryPayload["callTs"] as? TimeInterval,
let mediaStr = payload.dictionaryPayload["media"] as? String,
let media = CallMediaType(rawValue: mediaStr) {
let update = self.cxCallUpdate(contactId, displayName, media)
let callTs = Date(timeIntervalSince1970: callTsInterval)
if callTs.timeIntervalSinceNow >= -180 {
logger.debug("CallController: report pushkit call via CallKit")
let update = self.cxCallUpdate(invitation: invitation)
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
if error != nil {
m.callInvitations.removeValue(forKey: contactId)
@@ -205,11 +223,31 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
completion()
}
} else {
logger.debug("CallController will expire call 1")
self.reportExpiredCall(update: update, completion)
}
} else {
logger.debug("CallController will expire call 2")
self.reportExpiredCall(payload: payload, completion)
}
//DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
if (!ChatModel.shared.chatInitialized) {
logger.debug("CallController: initializing chat")
do {
try initializeChat(start: true, refreshInvitations: false)
} catch let error {
logger.error("CallController: initializing chat error: \(error)")
if let call = ChatModel.shared.activeCall {
self.endCall(call: call, completed: completion)
}
return
}
}
logger.debug("CallController: initialized chat")
startChatForCall()
logger.debug("CallController: started chat")
self.shouldSuspendChat = true
}
// This function fulfils the requirement to always report a call when PushKit notification is received,
@@ -239,8 +277,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID))")
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callUUID))")
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
if invitation.callTs.timeIntervalSinceNow >= -180 {
let update = cxCallUpdate(invitation: invitation)
provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
@@ -261,6 +299,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
return update
}
private func cxCallUpdate(_ contactId: String, _ displayName: String, _ media: CallMediaType) -> CXCallUpdate {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: contactId)
update.hasVideo = media == .video
update.localizedCallerName = displayName
return update
}
func reportIncomingCall(call: Call, connectedAt dateConnected: Date?) {
logger.debug("CallController: reporting incoming call connected")
if CallController.useCallKit() {
@@ -272,14 +318,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
logger.debug("CallController: reporting outgoing call connected")
if CallController.useCallKit(), let uuid = call.callkitUUID {
if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) {
provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected)
}
}
func reportCallRemoteEnded(invitation: RcvCallInvitation) {
logger.debug("CallController: reporting remote ended")
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
} else if invitation.contact.id == activeCallInvitation?.contact.id {
activeCallInvitation = nil
@@ -288,14 +334,17 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
func reportCallRemoteEnded(call: Call) {
logger.debug("CallController: reporting remote ended")
if CallController.useCallKit(), let uuid = call.callkitUUID {
if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) {
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
}
}
func startCall(_ contact: Contact, _ media: CallMediaType) {
logger.debug("CallController.startCall")
let uuid = callManager.newOutgoingCall(contact, media)
let callUUID = callManager.newOutgoingCall(contact, media)
guard let uuid = UUID(uuidString: callUUID) else {
return
}
if CallController.useCallKit() {
let handle = CXHandle(type: .generic, value: contact.id)
let action = CXStartCallAction(call: uuid, handle: handle)
@@ -307,19 +356,17 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
update.localizedCallerName = contact.displayName
self.provider.reportCall(with: uuid, updated: update)
}
} else if callManager.startOutgoingCall(callUUID: uuid) {
if callManager.startOutgoingCall(callUUID: uuid) {
logger.debug("CallController.startCall: call started")
} else {
logger.error("CallController.startCall: no active call")
}
} else if callManager.startOutgoingCall(callUUID: callUUID) {
logger.debug("CallController.startCall: call started")
} else {
logger.error("CallController.startCall: no active call")
}
}
func answerCall(invitation: RcvCallInvitation) {
logger.debug("CallController: answering a call")
if CallController.useCallKit(), let callUUID = invitation.callkitUUID {
requestTransaction(with: CXAnswerCallAction(call: callUUID))
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
requestTransaction(with: CXAnswerCallAction(call: uuid))
} else {
callManager.answerIncomingCall(invitation: invitation)
}
@@ -328,10 +375,13 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
}
func endCall(callUUID: UUID) {
logger.debug("CallController: ending the call with UUID \(callUUID.uuidString)")
func endCall(callUUID: String) {
let uuid = UUID(uuidString: callUUID)
logger.debug("CallController: ending the call with UUID \(callUUID)")
if CallController.useCallKit() {
requestTransaction(with: CXEndCallAction(call: callUUID))
if let uuid {
requestTransaction(with: CXEndCallAction(call: uuid))
}
} else {
callManager.endCall(callUUID: callUUID) { ok in
if ok {
+16 -16
View File
@@ -10,25 +10,25 @@ import Foundation
import SimpleXChat
class CallManager {
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID {
let uuid = UUID()
let call = Call(direction: .outgoing, contact: contact, callkitUUID: uuid, callState: .waitCapabilities, localMedia: media)
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> String {
let uuid = UUID().uuidString.lowercased()
let call = Call(direction: .outgoing, contact: contact, callUUID: uuid, callState: .waitCapabilities, initialCallType: media)
call.speakerEnabled = media == .video
ChatModel.shared.activeCall = call
return uuid
}
func startOutgoingCall(callUUID: UUID) -> Bool {
func startOutgoingCall(callUUID: String) -> Bool {
let m = ChatModel.shared
if let call = m.activeCall, call.callkitUUID == callUUID {
if let call = m.activeCall, call.callUUID == callUUID {
m.showCallView = true
Task { await m.callCommand.processCommand(.capabilities(media: call.localMedia)) }
Task { await m.callCommand.processCommand(.capabilities(media: call.initialCallType)) }
return true
}
return false
}
func answerIncomingCall(callUUID: UUID) -> Bool {
func answerIncomingCall(callUUID: String) -> Bool {
if let invitation = getCallInvitation(callUUID) {
answerIncomingCall(invitation: invitation)
return true
@@ -42,9 +42,9 @@ class CallManager {
let call = Call(
direction: .incoming,
contact: invitation.contact,
callkitUUID: invitation.callkitUUID,
callUUID: invitation.callUUID,
callState: .invitationAccepted,
localMedia: invitation.callType.media,
initialCallType: invitation.callType.media,
sharedKey: invitation.sharedKey
)
call.speakerEnabled = invitation.callType.media == .video
@@ -68,17 +68,17 @@ class CallManager {
}
}
func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool {
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
func enableMedia(source: CallMediaSource, enable: Bool, callUUID: String) -> Bool {
if let call = ChatModel.shared.activeCall, call.callUUID == callUUID {
let m = ChatModel.shared
Task { await m.callCommand.processCommand(.media(media: media, enable: enable)) }
Task { await m.callCommand.processCommand(.media(source: source, enable: enable)) }
return true
}
return false
}
func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) {
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
func endCall(callUUID: String, completed: @escaping (Bool) -> Void) {
if let call = ChatModel.shared.activeCall, call.callUUID == callUUID {
endCall(call: call) { completed(true) }
} else if let invitation = getCallInvitation(callUUID) {
endCall(invitation: invitation) { completed(true) }
@@ -126,8 +126,8 @@ class CallManager {
}
}
private func getCallInvitation(_ callUUID: UUID) -> RcvCallInvitation? {
if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callkitUUID == callUUID }) {
private func getCallInvitation(_ callUUID: String) -> RcvCallInvitation? {
if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callUUID == callUUID }) {
return invitation
}
return nil
@@ -10,40 +10,49 @@ import AVKit
struct CallViewRemote: UIViewRepresentable {
var client: WebRTCClient
var activeCall: Binding<WebRTCClient.Call?>
@ObservedObject var call: Call
@State var enablePip: (Bool) -> Void = {_ in }
@Binding var activeCallViewIsCollapsed: Bool
@Binding var contentMode: UIView.ContentMode
@Binding var pipShown: Bool
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, activeCallViewIsCollapsed: Binding<Bool>, pipShown: Binding<Bool>) {
self.client = client
self.activeCall = activeCall
self._activeCallViewIsCollapsed = activeCallViewIsCollapsed
self._pipShown = pipShown
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
if let call = activeCall.wrappedValue {
let remoteRenderer = RTCMTLVideoView(frame: view.frame)
remoteRenderer.videoContentMode = .scaleAspectFill
client.addRemoteRenderer(call, remoteRenderer)
addSubviewAndResize(remoteRenderer, into: view)
let remoteCameraRenderer = RTCMTLVideoView(frame: view.frame)
remoteCameraRenderer.videoContentMode = contentMode
remoteCameraRenderer.tag = 0
if AVPictureInPictureController.isPictureInPictureSupported() {
makeViewWithRTCRenderer(call, remoteRenderer, view, context)
}
let screenVideo = call.peerMediaSources.screenVideo
let remoteScreenRenderer = RTCMTLVideoView(frame: view.frame)
remoteScreenRenderer.videoContentMode = contentMode
remoteScreenRenderer.tag = 1
remoteScreenRenderer.alpha = screenVideo ? 1 : 0
context.coordinator.cameraRenderer = remoteCameraRenderer
context.coordinator.screenRenderer = remoteScreenRenderer
client.addRemoteCameraRenderer(remoteCameraRenderer)
client.addRemoteScreenRenderer(remoteScreenRenderer)
if screenVideo {
addSubviewAndResize(remoteScreenRenderer, remoteCameraRenderer, into: view)
} else {
addSubviewAndResize(remoteCameraRenderer, remoteScreenRenderer, into: view)
}
if AVPictureInPictureController.isPictureInPictureSupported() {
makeViewWithRTCRenderer(remoteCameraRenderer, remoteScreenRenderer, view, context)
}
return view
}
func makeViewWithRTCRenderer(_ call: WebRTCClient.Call, _ remoteRenderer: RTCMTLVideoView, _ view: UIView, _ context: Context) {
let pipRemoteRenderer = RTCMTLVideoView(frame: view.frame)
pipRemoteRenderer.videoContentMode = .scaleAspectFill
func makeViewWithRTCRenderer(_ remoteCameraRenderer: RTCMTLVideoView, _ remoteScreenRenderer: RTCMTLVideoView, _ view: UIView, _ context: Context) {
let pipRemoteCameraRenderer = RTCMTLVideoView(frame: view.frame)
pipRemoteCameraRenderer.videoContentMode = .scaleAspectFill
let pipRemoteScreenRenderer = RTCMTLVideoView(frame: view.frame)
pipRemoteScreenRenderer.videoContentMode = .scaleAspectFill
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
pipVideoCallViewController.preferredContentSize = CGSize(width: 1080, height: 1920)
addSubviewAndResize(pipRemoteRenderer, into: pipVideoCallViewController.view)
let pipContentSource = AVPictureInPictureController.ContentSource(
activeVideoCallSourceView: view,
contentViewController: pipVideoCallViewController
@@ -55,7 +64,9 @@ struct CallViewRemote: UIViewRepresentable {
context.coordinator.pipController = pipController
context.coordinator.willShowHide = { show in
if show {
client.addRemoteRenderer(call, pipRemoteRenderer)
client.addRemoteCameraRenderer(pipRemoteCameraRenderer)
client.addRemoteScreenRenderer(pipRemoteScreenRenderer)
context.coordinator.relayout()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
activeCallViewIsCollapsed = true
}
@@ -67,13 +78,29 @@ struct CallViewRemote: UIViewRepresentable {
}
context.coordinator.didShowHide = { show in
if show {
remoteRenderer.isHidden = true
remoteCameraRenderer.isHidden = true
remoteScreenRenderer.isHidden = true
} else {
client.removeRemoteRenderer(call, pipRemoteRenderer)
remoteRenderer.isHidden = false
client.removeRemoteCameraRenderer(pipRemoteCameraRenderer)
client.removeRemoteScreenRenderer(pipRemoteScreenRenderer)
remoteCameraRenderer.isHidden = false
remoteScreenRenderer.isHidden = false
}
pipShown = show
}
context.coordinator.relayout = {
let camera = call.peerMediaSources.camera
let screenVideo = call.peerMediaSources.screenVideo
pipRemoteCameraRenderer.alpha = camera ? 1 : 0
pipRemoteScreenRenderer.alpha = screenVideo ? 1 : 0
if screenVideo {
addSubviewAndResize(pipRemoteScreenRenderer, pipRemoteCameraRenderer, pip: true, into: pipVideoCallViewController.view)
} else {
addSubviewAndResize(pipRemoteCameraRenderer, pipRemoteScreenRenderer, pip: true, into: pipVideoCallViewController.view)
}
(pipVideoCallViewController.view.subviews[0] as! RTCMTLVideoView).videoContentMode = contentMode
(pipVideoCallViewController.view.subviews[1] as! RTCMTLVideoView).videoContentMode = .scaleAspectFill
}
DispatchQueue.main.async {
enablePip = { enable in
if enable != pipShown /* pipController.isPictureInPictureActive */ {
@@ -88,24 +115,50 @@ struct CallViewRemote: UIViewRepresentable {
}
func makeCoordinator() -> Coordinator {
Coordinator()
Coordinator(client)
}
func updateUIView(_ view: UIView, context: Context) {
logger.debug("CallView.updateUIView remote")
let camera = view.subviews.first(where: { $0.tag == 0 })!
let screen = view.subviews.first(where: { $0.tag == 1 })!
let screenVideo = call.peerMediaSources.screenVideo
if screenVideo && screen.alpha == 0 {
screen.alpha = 1
addSubviewAndResize(screen, camera, into: view)
} else if !screenVideo && screen.alpha == 1 {
screen.alpha = 0
addSubviewAndResize(camera, screen, into: view)
}
(view.subviews[0] as! RTCMTLVideoView).videoContentMode = contentMode
(view.subviews[1] as! RTCMTLVideoView).videoContentMode = .scaleAspectFill
camera.alpha = call.peerMediaSources.camera ? 1 : 0
screen.alpha = call.peerMediaSources.screenVideo ? 1 : 0
DispatchQueue.main.async {
if activeCallViewIsCollapsed != pipShown {
enablePip(activeCallViewIsCollapsed)
} else if pipShown {
context.coordinator.relayout()
}
}
}
// MARK: - Coordinator
class Coordinator: NSObject, AVPictureInPictureControllerDelegate {
var cameraRenderer: RTCMTLVideoView?
var screenRenderer: RTCMTLVideoView?
var client: WebRTCClient
var pipController: AVPictureInPictureController? = nil
var willShowHide: (Bool) -> Void = { _ in }
var didShowHide: (Bool) -> Void = { _ in }
var relayout: () -> Void = {}
required init(_ client: WebRTCClient) {
self.client = client
}
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
willShowHide(true)
}
@@ -127,11 +180,20 @@ struct CallViewRemote: UIViewRepresentable {
}
deinit {
// TODO: deinit is not called when changing call type from audio to video and back,
// which causes many renderers can be created and added to stream (if enabling/disabling
// video while not yet connected in outgoing call)
pipController?.stopPictureInPicture()
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
pipController?.contentSource = nil
pipController?.delegate = nil
pipController = nil
if let cameraRenderer {
client.removeRemoteCameraRenderer(cameraRenderer)
}
if let screenRenderer {
client.removeRemoteScreenRenderer(screenRenderer)
}
}
}
@@ -148,51 +210,109 @@ struct CallViewRemote: UIViewRepresentable {
struct CallViewLocal: UIViewRepresentable {
var client: WebRTCClient
var activeCall: Binding<WebRTCClient.Call?>
var localRendererAspectRatio: Binding<CGFloat?>
@State var pipStateChanged: (Bool) -> Void = {_ in }
@Binding var pipShown: Bool
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, localRendererAspectRatio: Binding<CGFloat?>, pipShown: Binding<Bool>) {
init(client: WebRTCClient, localRendererAspectRatio: Binding<CGFloat?>, pipShown: Binding<Bool>) {
self.client = client
self.activeCall = activeCall
self.localRendererAspectRatio = localRendererAspectRatio
self._pipShown = pipShown
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
if let call = activeCall.wrappedValue {
let localRenderer = RTCEAGLVideoView(frame: .zero)
client.addLocalRenderer(call, localRenderer)
client.startCaptureLocalVideo(call)
addSubviewAndResize(localRenderer, into: view)
DispatchQueue.main.async {
pipStateChanged = { shown in
localRenderer.isHidden = shown
}
let localRenderer = RTCEAGLVideoView(frame: .zero)
context.coordinator.renderer = localRenderer
client.addLocalRenderer(localRenderer)
addSubviewAndResize(localRenderer, nil, into: view)
DispatchQueue.main.async {
pipStateChanged = { shown in
localRenderer.isHidden = shown
}
}
return view
}
func makeCoordinator() -> Coordinator {
Coordinator(client)
}
func updateUIView(_ view: UIView, context: Context) {
logger.debug("CallView.updateUIView local")
pipStateChanged(pipShown)
}
// MARK: - Coordinator
class Coordinator: NSObject, AVPictureInPictureControllerDelegate {
var renderer: RTCEAGLVideoView?
var client: WebRTCClient
required init(_ client: WebRTCClient) {
self.client = client
}
deinit {
if let renderer {
client.removeLocalRenderer(renderer)
}
}
}
}
private func addSubviewAndResize(_ view: UIView, into containerView: UIView) {
containerView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|",
options: [],
metrics: nil,
views: ["view": view]))
private func addSubviewAndResize(_ fullscreen: UIView, _ end: UIView?, pip: Bool = false, into containerView: UIView) {
if containerView.subviews.firstIndex(of: fullscreen) == 0 && ((end == nil && containerView.subviews.count == 1) || (end != nil && containerView.subviews.firstIndex(of: end!) == 1)) {
// Nothing to do, elements on their places
return
}
containerView.removeConstraints(containerView.constraints)
containerView.subviews.forEach { sub in sub.removeFromSuperview()}
containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|",
containerView.addSubview(fullscreen)
fullscreen.translatesAutoresizingMaskIntoConstraints = false
fullscreen.layer.cornerRadius = 0
fullscreen.layer.masksToBounds = false
if let end {
containerView.addSubview(end)
end.translatesAutoresizingMaskIntoConstraints = false
end.layer.cornerRadius = pip ? 8 : 10
end.layer.masksToBounds = true
}
let constraintFullscreenV = NSLayoutConstraint.constraints(
withVisualFormat: "V:|[fullscreen]|",
options: [],
metrics: nil,
views: ["view": view]))
views: ["fullscreen": fullscreen]
)
let constraintFullscreenH = NSLayoutConstraint.constraints(
withVisualFormat: "H:|[fullscreen]|",
options: [],
metrics: nil,
views: ["fullscreen": fullscreen]
)
containerView.addConstraints(constraintFullscreenV)
containerView.addConstraints(constraintFullscreenH)
if let end {
let constraintEndWidth = NSLayoutConstraint(
item: end, attribute: .width, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: pip ? 0.5 : 0.3, constant: 0
)
let constraintEndHeight = NSLayoutConstraint(
item: end, attribute: .height, relatedBy: .equal, toItem: containerView, attribute: .width, multiplier: pip ? 0.5 * 1.33 : 0.3 * 1.33, constant: 0
)
let constraintEndX = NSLayoutConstraint(
item: end, attribute: .leading, relatedBy: .equal, toItem: containerView, attribute: .trailing, multiplier: pip ? 0.5 : 0.7, constant: pip ? -8 : -17
)
let constraintEndY = NSLayoutConstraint(
item: end, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1, constant: pip ? -8 : -92
)
containerView.addConstraint(constraintEndWidth)
containerView.addConstraint(constraintEndHeight)
containerView.addConstraint(constraintEndX)
containerView.addConstraint(constraintEndY)
}
containerView.layoutIfNeeded()
}
@@ -38,6 +38,7 @@ struct IncomingCallView: View {
}
HStack {
ProfilePreview(profileOf: invitation.contact, color: .white)
.padding(.vertical, 6)
Spacer()
callButton("Reject", "phone.down.fill", .red) {
+42 -20
View File
@@ -18,49 +18,49 @@ class Call: ObservableObject, Equatable {
var direction: CallDirection
var contact: Contact
var callkitUUID: UUID?
var localMedia: CallMediaType
var callUUID: String?
var initialCallType: CallMediaType
@Published var localMediaSources: CallMediaSources
@Published var callState: CallState
@Published var localCapabilities: CallCapabilities?
@Published var peerMedia: CallMediaType?
@Published var peerMediaSources: CallMediaSources = CallMediaSources()
@Published var sharedKey: String?
@Published var audioEnabled = true
@Published var speakerEnabled = false
@Published var videoEnabled: Bool
@Published var connectionInfo: ConnectionInfo?
@Published var connectedAt: Date? = nil
init(
direction: CallDirection,
contact: Contact,
callkitUUID: UUID?,
callUUID: String?,
callState: CallState,
localMedia: CallMediaType,
initialCallType: CallMediaType,
sharedKey: String? = nil
) {
self.direction = direction
self.contact = contact
self.callkitUUID = callkitUUID
self.callUUID = callUUID
self.callState = callState
self.localMedia = localMedia
self.initialCallType = initialCallType
self.sharedKey = sharedKey
self.videoEnabled = localMedia == .video
self.localMediaSources = CallMediaSources(
mic: AVCaptureDevice.authorizationStatus(for: .audio) == .authorized,
camera: initialCallType == .video && AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
}
var encrypted: Bool { get { localEncrypted && sharedKey != nil } }
var localEncrypted: Bool { get { localCapabilities?.encryption ?? false } }
private var localEncrypted: Bool { get { localCapabilities?.encryption ?? false } }
var encryptionStatus: LocalizedStringKey {
get {
switch callState {
case .waitCapabilities: return ""
case .invitationSent: return localEncrypted ? "e2e encrypted" : "no e2e encryption"
case .invitationAccepted: return sharedKey == nil ? "contact has no e2e encryption" : "contact has e2e encryption"
default: return !localEncrypted ? "no e2e encryption" : sharedKey == nil ? "contact has no e2e encryption" : "e2e encrypted"
default: return !localEncrypted ? "no e2e encryption" : sharedKey == nil ? "contact has no e2e encryption" : "e2e encrypted"
}
}
}
var hasMedia: Bool { get { callState == .offerSent || callState == .negotiated || callState == .connected } }
var supportsVideo: Bool { get { peerMedia == .video || localMedia == .video } }
var hasVideo: Bool { get { localMediaSources.hasVideo || peerMediaSources.hasVideo } }
}
enum CallDirection {
@@ -105,18 +105,28 @@ struct WVAPIMessage: Equatable, Decodable, Encodable {
var command: WCallCommand?
}
struct CallMediaSources: Equatable, Codable {
var mic: Bool = false
var camera: Bool = false
var screenAudio: Bool = false
var screenVideo: Bool = false
var hasVideo: Bool { get { camera || screenVideo } }
}
enum WCallCommand: Equatable, Encodable, Decodable {
case capabilities(media: CallMediaType)
case start(media: CallMediaType, aesKey: String? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
case offer(offer: String, iceCandidates: String, media: CallMediaType, aesKey: String? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
case answer(answer: String, iceCandidates: String)
case ice(iceCandidates: String)
case media(media: CallMediaType, enable: Bool)
case media(source: CallMediaSource, enable: Bool)
case end
enum CodingKeys: String, CodingKey {
case type
case media
case source
case aesKey
case offer
case answer
@@ -167,9 +177,9 @@ enum WCallCommand: Equatable, Encodable, Decodable {
case let .ice(iceCandidates):
try container.encode("ice", forKey: .type)
try container.encode(iceCandidates, forKey: .iceCandidates)
case let .media(media, enable):
case let .media(source, enable):
try container.encode("media", forKey: .type)
try container.encode(media, forKey: .media)
try container.encode(source, forKey: .media)
try container.encode(enable, forKey: .enable)
case .end:
try container.encode("end", forKey: .type)
@@ -205,9 +215,9 @@ enum WCallCommand: Equatable, Encodable, Decodable {
let iceCandidates = try container.decode(String.self, forKey: CodingKeys.iceCandidates)
self = .ice(iceCandidates: iceCandidates)
case "media":
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
let source = try container.decode(CallMediaSource.self, forKey: CodingKeys.source)
let enable = try container.decode(Bool.self, forKey: CodingKeys.enable)
self = .media(media: media, enable: enable)
self = .media(source: source, enable: enable)
case "end":
self = .end
default:
@@ -224,6 +234,7 @@ enum WCallResponse: Equatable, Decodable {
case ice(iceCandidates: String)
case connection(state: ConnectionState)
case connected(connectionInfo: ConnectionInfo)
case peerMedia(source: CallMediaSource, enabled: Bool)
case ended
case ok
case error(message: String)
@@ -238,6 +249,8 @@ enum WCallResponse: Equatable, Decodable {
case state
case connectionInfo
case message
case source
case enabled
}
var respType: String {
@@ -249,6 +262,7 @@ enum WCallResponse: Equatable, Decodable {
case .ice: return "ice"
case .connection: return "connection"
case .connected: return "connected"
case .peerMedia: return "peerMedia"
case .ended: return "ended"
case .ok: return "ok"
case .error: return "error"
@@ -283,6 +297,10 @@ enum WCallResponse: Equatable, Decodable {
case "connected":
let connectionInfo = try container.decode(ConnectionInfo.self, forKey: CodingKeys.connectionInfo)
self = .connected(connectionInfo: connectionInfo)
case "peerMedia":
let source = try container.decode(CallMediaSource.self, forKey: CodingKeys.source)
let enabled = try container.decode(Bool.self, forKey: CodingKeys.enabled)
self = .peerMedia(source: source, enabled: enabled)
case "ended":
self = .ended
case "ok":
@@ -324,6 +342,10 @@ extension WCallResponse: Encodable {
case let .connected(connectionInfo):
try container.encode("connected", forKey: .type)
try container.encode(connectionInfo, forKey: .connectionInfo)
case let .peerMedia(source, enabled):
try container.encode("peerMedia", forKey: .type)
try container.encode(source, forKey: .source)
try container.encode(enabled, forKey: .enabled)
case .ended:
try container.encode("ended", forKey: .type)
case .ok:
@@ -376,7 +398,7 @@ actor WebRTCCommandProcessor {
func shouldRunCommand(_ client: WebRTCClient, _ c: WCallCommand) -> Bool {
switch c {
case .capabilities, .start, .offer, .end: true
default: client.activeCall.wrappedValue != nil
default: client.activeCall != nil
}
}
}
+423 -103
View File
@@ -23,15 +23,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
struct Call {
var connection: RTCPeerConnection
var iceCandidates: IceCandidates
var localMedia: CallMediaType
var localCamera: RTCVideoCapturer?
var localVideoSource: RTCVideoSource?
var localStream: RTCVideoTrack?
var remoteStream: RTCVideoTrack?
var device: AVCaptureDevice.Position = .front
var localAudioTrack: RTCAudioTrack?
var localVideoTrack: RTCVideoTrack?
var remoteAudioTrack: RTCAudioTrack?
var remoteVideoTrack: RTCVideoTrack?
var remoteScreenAudioTrack: RTCAudioTrack?
var remoteScreenVideoTrack: RTCVideoTrack?
var device: AVCaptureDevice.Position
var aesKey: String?
var frameEncryptor: RTCFrameEncryptor?
var frameDecryptor: RTCFrameDecryptor?
var peerHasOldVersion: Bool
}
struct NotConnectedCall {
var audioTrack: RTCAudioTrack?
var localCameraAndTrack: (RTCVideoCapturer, RTCVideoTrack)?
var device: AVCaptureDevice.Position = .front
}
actor IceCandidates {
@@ -51,17 +60,20 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
private let rtcAudioSession = RTCAudioSession.sharedInstance()
private let audioQueue = DispatchQueue(label: "chat.simplex.app.audio")
private var sendCallResponse: (WVAPIMessage) async -> Void
var activeCall: Binding<Call?>
var activeCall: Call?
var notConnectedCall: NotConnectedCall?
private var localRendererAspectRatio: Binding<CGFloat?>
var cameraRenderers: [RTCVideoRenderer] = []
var screenRenderers: [RTCVideoRenderer] = []
@available(*, unavailable)
override init() {
fatalError("Unimplemented")
}
required init(_ activeCall: Binding<Call?>, _ sendCallResponse: @escaping (WVAPIMessage) async -> Void, _ localRendererAspectRatio: Binding<CGFloat?>) {
required init(_ sendCallResponse: @escaping (WVAPIMessage) async -> Void, _ localRendererAspectRatio: Binding<CGFloat?>) {
self.sendCallResponse = sendCallResponse
self.activeCall = activeCall
self.localRendererAspectRatio = localRendererAspectRatio
rtcAudioSession.useManualAudio = CallController.useCallKit()
rtcAudioSession.isAudioEnabled = !CallController.useCallKit()
@@ -78,39 +90,45 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
let connection = createPeerConnection(iceServers ?? getWebRTCIceServers() ?? defaultIceServers, relay)
connection.delegate = self
createAudioSender(connection)
var localStream: RTCVideoTrack? = nil
var remoteStream: RTCVideoTrack? = nil
let device = notConnectedCall?.device ?? .front
var localCamera: RTCVideoCapturer? = nil
var localVideoSource: RTCVideoSource? = nil
if mediaType == .video {
(localStream, remoteStream, localCamera, localVideoSource) = createVideoSender(connection)
var localAudioTrack: RTCAudioTrack? = nil
var localVideoTrack: RTCVideoTrack? = nil
if let localCameraAndTrack = notConnectedCall?.localCameraAndTrack {
(localCamera, localVideoTrack) = localCameraAndTrack
} else if notConnectedCall == nil && mediaType == .video {
(localCamera, localVideoTrack) = createVideoTrackAndStartCapture(device)
}
if let audioTrack = notConnectedCall?.audioTrack {
localAudioTrack = audioTrack
} else if notConnectedCall == nil {
localAudioTrack = createAudioTrack()
}
notConnectedCall?.localCameraAndTrack = nil
notConnectedCall?.audioTrack = nil
var frameEncryptor: RTCFrameEncryptor? = nil
var frameDecryptor: RTCFrameDecryptor? = nil
if aesKey != nil {
let encryptor = RTCFrameEncryptor.init(sizeChange: Int32(WebRTCClient.ivTagBytes))
encryptor.delegate = self
frameEncryptor = encryptor
connection.senders.forEach { $0.setRtcFrameEncryptor(encryptor) }
let decryptor = RTCFrameDecryptor.init(sizeChange: -Int32(WebRTCClient.ivTagBytes))
decryptor.delegate = self
frameDecryptor = decryptor
// Has no video receiver in outgoing call if applied here, see [peerConnection(_ connection: RTCPeerConnection, didChange newState]
// connection.receivers.forEach { $0.setRtcFrameDecryptor(decryptor) }
}
return Call(
connection: connection,
iceCandidates: IceCandidates(),
localMedia: mediaType,
localCamera: localCamera,
localVideoSource: localVideoSource,
localStream: localStream,
remoteStream: remoteStream,
localAudioTrack: localAudioTrack,
localVideoTrack: localVideoTrack,
device: device,
aesKey: aesKey,
frameEncryptor: frameEncryptor,
frameDecryptor: frameDecryptor
frameDecryptor: frameDecryptor,
peerHasOldVersion: false
)
}
@@ -151,18 +169,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func sendCallCommand(command: WCallCommand) async {
var resp: WCallResponse? = nil
let pc = activeCall.wrappedValue?.connection
let pc = activeCall?.connection
switch command {
case .capabilities:
case let .capabilities(media): // outgoing
let localCameraAndTrack: (RTCVideoCapturer, RTCVideoTrack)? = media == .video
? createVideoTrackAndStartCapture(.front)
: nil
notConnectedCall = NotConnectedCall(audioTrack: createAudioTrack(), localCameraAndTrack: localCameraAndTrack, device: .front)
resp = .capabilities(capabilities: CallCapabilities(encryption: WebRTCClient.enableEncryption))
case let .start(media: media, aesKey, iceServers, relay):
case let .start(media: media, aesKey, iceServers, relay): // incoming
logger.debug("starting incoming call - create webrtc session")
if activeCall.wrappedValue != nil { endCall() }
if activeCall != nil { endCall() }
let encryption = WebRTCClient.enableEncryption
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, encryption ? aesKey : nil, relay)
activeCall.wrappedValue = call
activeCall = call
setupLocalTracks(true, call)
let (offer, error) = await call.connection.offer()
if let offer = offer {
setupEncryptionForLocalTracks(call)
resp = .offer(
offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: offer.type.toSdpType(), sdp: offer.sdp))),
iceCandidates: compressToBase64(input: encodeJSON(await self.getInitialIceCandidates())),
@@ -172,18 +196,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
} else {
resp = .error(message: "offer error: \(error?.localizedDescription ?? "unknown error")")
}
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay):
if activeCall.wrappedValue != nil {
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay): // outgoing
if activeCall != nil {
resp = .error(message: "accept: call already started")
} else if !WebRTCClient.enableEncryption && aesKey != nil {
resp = .error(message: "accept: encryption is not supported")
} else if let offer: CustomRTCSessionDescription = decodeJSON(decompressFromBase64(input: offer)),
let remoteIceCandidates: [RTCIceCandidate] = decodeJSON(decompressFromBase64(input: iceCandidates)) {
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, WebRTCClient.enableEncryption ? aesKey : nil, relay)
activeCall.wrappedValue = call
activeCall = call
let pc = call.connection
if let type = offer.type, let sdp = offer.sdp {
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
setupLocalTracks(false, call)
setupEncryptionForLocalTracks(call)
pc.transceivers.forEach { transceiver in
transceiver.setDirection(.sendRecv, error: nil)
}
await adaptToOldVersion(pc.transceivers.count <= 2)
let (answer, error) = await pc.answer()
if let answer = answer {
self.addIceCandidates(pc, remoteIceCandidates)
@@ -200,7 +230,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
}
}
case let .answer(answer, iceCandidates):
case let .answer(answer, iceCandidates): // incoming
if pc == nil {
resp = .error(message: "answer: call not started")
} else if pc?.localDescription == nil {
@@ -212,6 +242,9 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
let type = answer.type, let sdp = answer.sdp,
let pc = pc {
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
var currentDirection: RTCRtpTransceiverDirection = .sendOnly
pc.transceivers[2].currentDirection(&currentDirection)
await adaptToOldVersion(currentDirection == .sendOnly)
addIceCandidates(pc, remoteIceCandidates)
resp = .ok
} else {
@@ -226,13 +259,11 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
} else {
resp = .error(message: "ice: call not started")
}
case let .media(media, enable):
if activeCall.wrappedValue == nil {
case let .media(source, enable):
if activeCall == nil {
resp = .error(message: "media: call not started")
} else if activeCall.wrappedValue?.localMedia == .audio && media == .video {
resp = .error(message: "media: no video")
} else {
enableMedia(media, enable)
await enableMedia(source, enable)
resp = .ok
}
case .end:
@@ -247,7 +278,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func getInitialIceCandidates() async -> [RTCIceCandidate] {
await untilIceComplete(timeoutMs: 750, stepMs: 150) {}
let candidates = await activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
let candidates = await activeCall?.iceCandidates.getAndClear() ?? []
logger.debug("WebRTCClient: sending initial ice candidates: \(candidates.count)")
return candidates
}
@@ -255,7 +286,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func waitForMoreIceCandidates() {
Task {
await untilIceComplete(timeoutMs: 12000, stepMs: 1500) {
let candidates = await self.activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
let candidates = await self.activeCall?.iceCandidates.getAndClear() ?? []
if candidates.count > 0 {
logger.debug("WebRTCClient: sending more ice candidates: \(candidates.count)")
await self.sendIceCandidates(candidates)
@@ -272,25 +303,202 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
)
}
func enableMedia(_ media: CallMediaType, _ enable: Bool) {
logger.debug("WebRTCClient: enabling media \(media.rawValue) \(enable)")
media == .video ? setVideoEnabled(enable) : setAudioEnabled(enable)
func setupMuteUnmuteListener(_ transceiver: RTCRtpTransceiver, _ track: RTCMediaStreamTrack) {
// logger.log("Setting up mute/unmute listener in the call without encryption for mid = \(transceiver.mid)")
Task {
var lastBytesReceived: Int64 = 0
// muted initially
var mutedSeconds = 4
while let call = self.activeCall, transceiver.receiver.track?.readyState == .live {
let stats: RTCStatisticsReport = await call.connection.statistics(for: transceiver.receiver)
let stat = stats.statistics.values.first(where: { stat in stat.type == "inbound-rtp"})
if let stat {
//logger.debug("Stat \(stat.debugDescription)")
let bytes = stat.values["bytesReceived"] as! Int64
if bytes <= lastBytesReceived {
mutedSeconds += 1
if mutedSeconds == 3 {
await MainActor.run {
self.onMediaMuteUnmute(transceiver.mid, true)
}
}
} else {
if mutedSeconds >= 3 {
await MainActor.run {
self.onMediaMuteUnmute(transceiver.mid, false)
}
}
lastBytesReceived = bytes
mutedSeconds = 0
}
}
try? await Task.sleep(nanoseconds: 1000_000000)
}
}
}
func addLocalRenderer(_ activeCall: Call, _ renderer: RTCEAGLVideoView) {
activeCall.localStream?.add(renderer)
@MainActor
func onMediaMuteUnmute(_ transceiverMid: String?, _ mute: Bool) {
guard let activeCall = ChatModel.shared.activeCall else { return }
let source = mediaSourceFromTransceiverMid(transceiverMid)
logger.log("Mute/unmute \(source.rawValue) track = \(mute) with mid = \(transceiverMid ?? "nil")")
if source == .mic && activeCall.peerMediaSources.mic == mute {
activeCall.peerMediaSources.mic = !mute
} else if (source == .camera && activeCall.peerMediaSources.camera == mute) {
activeCall.peerMediaSources.camera = !mute
} else if (source == .screenAudio && activeCall.peerMediaSources.screenAudio == mute) {
activeCall.peerMediaSources.screenAudio = !mute
} else if (source == .screenVideo && activeCall.peerMediaSources.screenVideo == mute) {
activeCall.peerMediaSources.screenVideo = !mute
}
}
@MainActor
func enableMedia(_ source: CallMediaSource, _ enable: Bool) {
logger.debug("WebRTCClient: enabling media \(source.rawValue) \(enable)")
source == .camera ? setCameraEnabled(enable) : setAudioEnabled(enable)
}
@MainActor
func adaptToOldVersion(_ peerHasOldVersion: Bool) {
activeCall?.peerHasOldVersion = peerHasOldVersion
if peerHasOldVersion {
logger.debug("The peer has an old version. Remote audio track is nil = \(self.activeCall?.remoteAudioTrack == nil), video = \(self.activeCall?.remoteVideoTrack == nil)")
onMediaMuteUnmute("0", false)
if activeCall?.remoteVideoTrack != nil {
onMediaMuteUnmute("1", false)
}
if ChatModel.shared.activeCall?.localMediaSources.camera == true && ChatModel.shared.activeCall?.peerMediaSources.camera == false {
logger.debug("Stopping video track for the old version")
activeCall?.connection.senders[1].track = nil
ChatModel.shared.activeCall?.localMediaSources.camera = false
(activeCall?.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
activeCall?.localCamera = nil
activeCall?.localVideoTrack = nil
}
}
}
func addLocalRenderer(_ renderer: RTCEAGLVideoView) {
if let activeCall {
if let track = activeCall.localVideoTrack {
track.add(renderer)
}
} else if let notConnectedCall {
if let track = notConnectedCall.localCameraAndTrack?.1 {
track.add(renderer)
}
}
// To get width and height of a frame, see videoView(videoView:, didChangeVideoSize)
renderer.delegate = self
}
func removeLocalRenderer(_ renderer: RTCEAGLVideoView) {
if let activeCall {
if let track = activeCall.localVideoTrack {
track.remove(renderer)
}
} else if let notConnectedCall {
if let track = notConnectedCall.localCameraAndTrack?.1 {
track.remove(renderer)
}
}
renderer.delegate = nil
}
func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) {
guard size.height > 0 else { return }
localRendererAspectRatio.wrappedValue = size.width / size.height
}
func setupLocalTracks(_ incomingCall: Bool, _ call: Call) {
let pc = call.connection
let transceivers = call.connection.transceivers
let audioTrack = call.localAudioTrack
let videoTrack = call.localVideoTrack
if incomingCall {
let micCameraInit = RTCRtpTransceiverInit()
// streamIds required for old versions which adds tracks from stream, not from track property
micCameraInit.streamIds = ["micCamera"]
let screenAudioVideoInit = RTCRtpTransceiverInit()
screenAudioVideoInit.streamIds = ["screenAudioVideo"]
// incoming call, no transceivers yet. But they should be added in order: mic, camera, screen audio, screen video
// mid = 0, mic
if let audioTrack {
pc.addTransceiver(with: audioTrack, init: micCameraInit)
} else {
pc.addTransceiver(of: .audio, init: micCameraInit)
}
// mid = 1, camera
if let videoTrack {
pc.addTransceiver(with: videoTrack, init: micCameraInit)
} else {
pc.addTransceiver(of: .video, init: micCameraInit)
}
// mid = 2, screenAudio
pc.addTransceiver(of: .audio, init: screenAudioVideoInit)
// mid = 3, screenVideo
pc.addTransceiver(of: .video, init: screenAudioVideoInit)
} else {
// new version
if transceivers.count > 2 {
// Outgoing call. All transceivers are ready. Don't addTrack() because it will create new transceivers, replace existing (nil) tracks
transceivers
.first(where: { elem in mediaSourceFromTransceiverMid(elem.mid) == .mic })?
.sender.track = audioTrack
transceivers
.first(where: { elem in mediaSourceFromTransceiverMid(elem.mid) == .camera })?
.sender.track = videoTrack
} else {
// old version, only two transceivers
if let audioTrack {
pc.add(audioTrack, streamIds: ["micCamera"])
} else {
// it's important to have any track in order to be able to turn it on again (currently it's off)
let sender = pc.add(createAudioTrack(), streamIds: ["micCamera"])
sender?.track = nil
}
if let videoTrack {
pc.add(videoTrack, streamIds: ["micCamera"])
} else {
// it's important to have any track in order to be able to turn it on again (currently it's off)
let localVideoSource = WebRTCClient.factory.videoSource()
let localVideoTrack = WebRTCClient.factory.videoTrack(with: localVideoSource, trackId: "video0")
let sender = pc.add(localVideoTrack, streamIds: ["micCamera"])
sender?.track = nil
}
}
}
}
func mediaSourceFromTransceiverMid(_ mid: String?) -> CallMediaSource {
switch mid {
case "0":
return .mic
case "1":
return .camera
case "2":
return .screenAudio
case "3":
return .screenVideo
default:
return .unknown
}
}
// Should be called after local description set
func setupEncryptionForLocalTracks(_ call: Call) {
if let encryptor = call.frameEncryptor {
call.connection.senders.forEach { $0.setRtcFrameEncryptor(encryptor) }
}
}
func frameDecryptor(_ decryptor: RTCFrameDecryptor, mediaType: RTCRtpMediaType, withFrame encrypted: Data) -> Data? {
guard encrypted.count > 0 else { return nil }
if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8),
if var key: [CChar] = activeCall?.aesKey?.cString(using: .utf8),
let pointer: UnsafeMutableRawPointer = malloc(encrypted.count) {
memcpy(pointer, (encrypted as NSData).bytes, encrypted.count)
let isKeyFrame = encrypted[0] & 1 == 0
@@ -304,7 +512,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
func frameEncryptor(_ encryptor: RTCFrameEncryptor, mediaType: RTCRtpMediaType, withFrame unencrypted: Data) -> Data? {
guard unencrypted.count > 0 else { return nil }
if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8),
if var key: [CChar] = activeCall?.aesKey?.cString(using: .utf8),
let pointer: UnsafeMutableRawPointer = malloc(unencrypted.count + WebRTCClient.ivTagBytes) {
memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count)
let isKeyFrame = unencrypted[0] & 1 == 0
@@ -327,18 +535,42 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
}
func addRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) {
activeCall.remoteStream?.add(renderer)
func addRemoteCameraRenderer(_ renderer: RTCVideoRenderer) {
if activeCall?.remoteVideoTrack != nil {
activeCall?.remoteVideoTrack?.add(renderer)
} else {
cameraRenderers.append(renderer)
}
}
func removeRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) {
activeCall.remoteStream?.remove(renderer)
func removeRemoteCameraRenderer(_ renderer: RTCVideoRenderer) {
if activeCall?.remoteVideoTrack != nil {
activeCall?.remoteVideoTrack?.remove(renderer)
} else {
cameraRenderers.removeAll(where: { $0.isEqual(renderer) })
}
}
func startCaptureLocalVideo(_ activeCall: Call) {
func addRemoteScreenRenderer(_ renderer: RTCVideoRenderer) {
if activeCall?.remoteScreenVideoTrack != nil {
activeCall?.remoteScreenVideoTrack?.add(renderer)
} else {
screenRenderers.append(renderer)
}
}
func removeRemoteScreenRenderer(_ renderer: RTCVideoRenderer) {
if activeCall?.remoteScreenVideoTrack != nil {
activeCall?.remoteScreenVideoTrack?.remove(renderer)
} else {
screenRenderers.removeAll(where: { $0.isEqual(renderer) })
}
}
func startCaptureLocalVideo(_ device: AVCaptureDevice.Position?, _ capturer: RTCVideoCapturer?) {
#if targetEnvironment(simulator)
guard
let capturer = activeCall.localCamera as? RTCFileVideoCapturer
let capturer = (activeCall?.localCamera ?? notConnectedCall?.localCameraAndTrack?.0) as? RTCFileVideoCapturer
else {
logger.error("Unable to work with a file capturer")
return
@@ -348,10 +580,10 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
capturer.startCapturing(fromFileNamed: "sounds/video.mp4")
#else
guard
let capturer = activeCall.localCamera as? RTCCameraVideoCapturer,
let camera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == activeCall.device })
let capturer = capturer as? RTCCameraVideoCapturer,
let camera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == device })
else {
logger.error("Unable to find a camera")
logger.error("Unable to find a camera or local track")
return
}
@@ -377,19 +609,6 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
#endif
}
private func createAudioSender(_ connection: RTCPeerConnection) {
let streamId = "stream"
let audioTrack = createAudioTrack()
connection.add(audioTrack, streamIds: [streamId])
}
private func createVideoSender(_ connection: RTCPeerConnection) -> (RTCVideoTrack?, RTCVideoTrack?, RTCVideoCapturer?, RTCVideoSource?) {
let streamId = "stream"
let (localVideoTrack, localCamera, localVideoSource) = createVideoTrack()
connection.add(localVideoTrack, streamIds: [streamId])
return (localVideoTrack, connection.transceivers.first { $0.mediaType == .video }?.receiver.track as? RTCVideoTrack, localCamera, localVideoSource)
}
private func createAudioTrack() -> RTCAudioTrack {
let audioConstrains = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
let audioSource = WebRTCClient.factory.audioSource(with: audioConstrains)
@@ -397,7 +616,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
return audioTrack
}
private func createVideoTrack() -> (RTCVideoTrack, RTCVideoCapturer, RTCVideoSource) {
private func createVideoTrackAndStartCapture(_ device: AVCaptureDevice.Position) -> (RTCVideoCapturer, RTCVideoTrack) {
let localVideoSource = WebRTCClient.factory.videoSource()
#if targetEnvironment(simulator)
@@ -407,19 +626,30 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
#endif
let localVideoTrack = WebRTCClient.factory.videoTrack(with: localVideoSource, trackId: "video0")
return (localVideoTrack, localCamera, localVideoSource)
startCaptureLocalVideo(device, localCamera)
return (localCamera, localVideoTrack)
}
func endCall() {
guard let call = activeCall.wrappedValue else { return }
if #available(iOS 16.0, *) {
_endCall()
} else {
// Fixes `connection.close()` getting locked up in iOS15
DispatchQueue.global(qos: .utility).async { self._endCall() }
}
}
private func _endCall() {
(notConnectedCall?.localCameraAndTrack?.0 as? RTCCameraVideoCapturer)?.stopCapture()
guard let call = activeCall else { return }
logger.debug("WebRTCClient: ending the call")
activeCall.wrappedValue = nil
(call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
call.connection.close()
call.connection.delegate = nil
call.frameEncryptor?.delegate = nil
call.frameDecryptor?.delegate = nil
(call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
audioSessionToDefaults()
activeCall = nil
}
func untilIceComplete(timeoutMs: UInt64, stepMs: UInt64, action: @escaping () async -> Void) async {
@@ -428,7 +658,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
_ = try? await Task.sleep(nanoseconds: stepMs * 1000000)
t += stepMs
await action()
} while t < timeoutMs && activeCall.wrappedValue?.connection.iceGatheringState != .complete
} while t < timeoutMs && activeCall?.connection.iceGatheringState != .complete
}
}
@@ -489,11 +719,40 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
logger.debug("Connection should negotiate")
}
func peerConnection(_ peerConnection: RTCPeerConnection, didStartReceivingOn transceiver: RTCRtpTransceiver) {
if let track = transceiver.receiver.track {
DispatchQueue.main.async {
// Doesn't work for outgoing video call (audio in video call works ok still, same as incoming call)
// if let decryptor = self.activeCall?.frameDecryptor {
// transceiver.receiver.setRtcFrameDecryptor(decryptor)
// }
let source = self.mediaSourceFromTransceiverMid(transceiver.mid)
switch source {
case .mic: self.activeCall?.remoteAudioTrack = track as? RTCAudioTrack
case .camera:
self.activeCall?.remoteVideoTrack = track as? RTCVideoTrack
self.cameraRenderers.forEach({ renderer in
self.activeCall?.remoteVideoTrack?.add(renderer)
})
self.cameraRenderers.removeAll()
case .screenAudio: self.activeCall?.remoteScreenAudioTrack = track as? RTCAudioTrack
case .screenVideo:
self.activeCall?.remoteScreenVideoTrack = track as? RTCVideoTrack
self.screenRenderers.forEach({ renderer in
self.activeCall?.remoteScreenVideoTrack?.add(renderer)
})
self.screenRenderers.removeAll()
case .unknown: ()
}
}
self.setupMuteUnmuteListener(transceiver, track)
}
}
func peerConnection(_ connection: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
debugPrint("Connection new connection state: \(newState.toString() ?? "" + newState.rawValue.description) \(connection.receivers)")
guard let call = activeCall.wrappedValue,
let connectionStateString = newState.toString(),
guard let connectionStateString = newState.toString(),
let iceConnectionStateString = connection.iceConnectionState.toString(),
let iceGatheringStateString = connection.iceGatheringState.toString(),
let signalingStateString = connection.signalingState.toString()
@@ -514,18 +773,14 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
switch newState {
case .checking:
if let frameDecryptor = activeCall.wrappedValue?.frameDecryptor {
if let frameDecryptor = activeCall?.frameDecryptor {
connection.receivers.forEach { $0.setRtcFrameDecryptor(frameDecryptor) }
}
let enableSpeaker: Bool
switch call.localMedia {
case .video: enableSpeaker = true
default: enableSpeaker = false
}
let enableSpeaker: Bool = ChatModel.shared.activeCall?.localMediaSources.hasVideo == true
setSpeakerEnabledAndConfigureSession(enableSpeaker)
case .connected: sendConnectedEvent(connection)
case .disconnected, .failed: endCall()
default: do {}
default: ()
}
}
}
@@ -537,7 +792,7 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
func peerConnection(_ connection: RTCPeerConnection, didGenerate candidate: WebRTC.RTCIceCandidate) {
// logger.debug("Connection generated candidate \(candidate.debugDescription)")
Task {
await self.activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil))
await self.activeCall?.iceCandidates.append(candidate.toCandidate(nil, nil))
}
}
@@ -592,11 +847,42 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
}
extension WebRTCClient {
func setAudioEnabled(_ enabled: Bool) {
setTrackEnabled(RTCAudioTrack.self, enabled)
static func isAuthorized(for type: AVMediaType) async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: type)
var isAuthorized = status == .authorized
if status == .notDetermined {
isAuthorized = await AVCaptureDevice.requestAccess(for: type)
}
return isAuthorized
}
func setSpeakerEnabledAndConfigureSession( _ enabled: Bool) {
static func showUnauthorizedAlert(for type: AVMediaType) {
if type == .audio {
AlertManager.shared.showAlert(Alert(
title: Text("No permission to record speech"),
message: Text("To record speech please grant permission to use Microphone."),
primaryButton: .default(Text("Open Settings")) {
DispatchQueue.main.async {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
}
},
secondaryButton: .cancel()
))
} else if type == .video {
AlertManager.shared.showAlert(Alert(
title: Text("No permission to record video"),
message: Text("To record video please grant permission to use Camera."),
primaryButton: .default(Text("Open Settings")) {
DispatchQueue.main.async {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
}
},
secondaryButton: .cancel()
))
}
}
func setSpeakerEnabledAndConfigureSession( _ enabled: Bool, skipExternalDevice: Bool = false) {
logger.debug("WebRTCClient: configuring session with speaker enabled \(enabled)")
audioQueue.async { [weak self] in
guard let self = self else { return }
@@ -609,7 +895,7 @@ extension WebRTCClient {
if enabled {
try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue, with: [.defaultToSpeaker, .allowBluetooth, .allowAirPlay, .allowBluetoothA2DP])
try self.rtcAudioSession.setMode(AVAudioSession.Mode.videoChat.rawValue)
if hasExternalAudioDevice, let preferred = self.rtcAudioSession.session.preferredInputDevice() {
if hasExternalAudioDevice && !skipExternalDevice, let preferred = self.rtcAudioSession.session.preferredInputDevice() {
try self.rtcAudioSession.setPreferredInput(preferred)
} else {
try self.rtcAudioSession.overrideOutputAudioPort(.speaker)
@@ -619,7 +905,7 @@ extension WebRTCClient {
try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue)
try self.rtcAudioSession.overrideOutputAudioPort(.none)
}
if hasExternalAudioDevice {
if hasExternalAudioDevice && !skipExternalDevice {
logger.debug("WebRTCClient: configuring session with external device available, skip configuring speaker")
}
try self.rtcAudioSession.setActive(true)
@@ -650,25 +936,59 @@ extension WebRTCClient {
}
}
func setVideoEnabled(_ enabled: Bool) {
setTrackEnabled(RTCVideoTrack.self, enabled)
@MainActor
func setAudioEnabled(_ enabled: Bool) {
if activeCall != nil {
activeCall?.localAudioTrack = enabled ? createAudioTrack() : nil
activeCall?.connection.transceivers.first(where: { t in mediaSourceFromTransceiverMid(t.mid) == .mic })?.sender.track = activeCall?.localAudioTrack
} else if notConnectedCall != nil {
notConnectedCall?.audioTrack = enabled ? createAudioTrack() : nil
}
ChatModel.shared.activeCall?.localMediaSources.mic = enabled
}
@MainActor
func setCameraEnabled(_ enabled: Bool) {
if let call = activeCall {
if enabled {
if call.localVideoTrack == nil {
let device = activeCall?.device ?? notConnectedCall?.device ?? .front
let (camera, track) = createVideoTrackAndStartCapture(device)
activeCall?.localCamera = camera
activeCall?.localVideoTrack = track
}
} else {
(call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
activeCall?.localCamera = nil
activeCall?.localVideoTrack = nil
}
call.connection.transceivers
.first(where: { t in mediaSourceFromTransceiverMid(t.mid) == .camera })?
.sender.track = activeCall?.localVideoTrack
ChatModel.shared.activeCall?.localMediaSources.camera = activeCall?.localVideoTrack != nil
} else if let call = notConnectedCall {
if enabled {
let device = activeCall?.device ?? notConnectedCall?.device ?? .front
notConnectedCall?.localCameraAndTrack = createVideoTrackAndStartCapture(device)
} else {
(call.localCameraAndTrack?.0 as? RTCCameraVideoCapturer)?.stopCapture()
notConnectedCall?.localCameraAndTrack = nil
}
ChatModel.shared.activeCall?.localMediaSources.camera = notConnectedCall?.localCameraAndTrack != nil
}
}
func flipCamera() {
switch activeCall.wrappedValue?.device {
case .front: activeCall.wrappedValue?.device = .back
case .back: activeCall.wrappedValue?.device = .front
default: ()
let device = activeCall?.device ?? notConnectedCall?.device
if activeCall != nil {
activeCall?.device = device == .front ? .back : .front
} else {
notConnectedCall?.device = device == .front ? .back : .front
}
if let call = activeCall.wrappedValue {
startCaptureLocalVideo(call)
}
}
private func setTrackEnabled<T: RTCMediaStreamTrack>(_ type: T.Type, _ enabled: Bool) {
activeCall.wrappedValue?.connection.transceivers
.compactMap { $0.sender.track as? T }
.forEach { $0.isEnabled = enabled }
startCaptureLocalVideo(
activeCall?.device ?? notConnectedCall?.device,
(activeCall?.localCamera ?? notConnectedCall?.localCameraAndTrack?.0) as? RTCCameraVideoCapturer
)
}
}
@@ -671,7 +671,14 @@ private struct CallButton: View {
InfoViewButton(image: image, title: title, disabledLook: !canCall, width: width) {
if canCall {
CallController.shared.startCall(contact, mediaType)
if CallController.useCallKit() {
CallController.shared.startCall(contact, mediaType)
} else {
// When CallKit is not used, colorscheme will be changed and it will be visible if not hiding sheets first
dismissAllSheets(animated: true) {
CallController.shared.startCall(contact, mediaType)
}
}
} else if contact.nextSendGrpInv {
showAlert(SomeAlert(
alert: mkAlert(
@@ -935,7 +942,7 @@ func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Al
)
}
func queueInfoText(_ info: (RcvMsgInfo?, QueueInfo)) -> String {
func queueInfoText(_ info: (RcvMsgInfo?, ServerQueueInfo)) -> String {
let (rcvMsgInfo, qInfo) = info
var msgInfo: String
if let rcvMsgInfo { msgInfo = encodeJSON(rcvMsgInfo) } else { msgInfo = "none" }
@@ -12,6 +12,7 @@ import SimpleXChat
struct CIGroupInvitationView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.showTimestamp) var showTimestamp: Bool
@ObservedObject var chat: Chat
var chatItem: ChatItem
var groupInvitation: CIGroupInvitation
@@ -45,7 +46,7 @@ struct CIGroupInvitationView: View {
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
.overlay(DetermineWidth())
}
@@ -53,7 +54,7 @@ struct CIGroupInvitationView: View {
(
groupInvitationText()
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
.overlay(DetermineWidth())
}
@@ -69,7 +70,7 @@ struct CIGroupInvitationView: View {
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(chatItem, theme))
.background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
.textSelection(.disabled)
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
.onChange(of: inProgress) { inProgress in
@@ -165,9 +165,9 @@ struct CIImageView: View {
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
Image(systemName: icon)
.resizable()
.invertedForegroundStyle()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.foregroundColor(.white)
.padding(padding)
}
@@ -16,7 +16,7 @@ struct CILinkView: View {
var body: some View {
VStack(alignment: .center, spacing: 6) {
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
if let uiImage = imageFromBase64(linkPreview.image) {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
@@ -12,11 +12,13 @@ import SimpleXChat
struct CIMetaView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
@Environment(\.showTimestamp) var showTimestamp: Bool
var chatItem: ChatItem
var metaColor: Color
var paleMetaColor = Color(UIColor.tertiaryLabel)
var showStatus = true
var showEdited = true
var invertedMaterial = false
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
@@ -24,93 +26,143 @@ struct CIMetaView: View {
if chatItem.isDeletedContent {
chatItem.timestampText.font(.caption).foregroundColor(metaColor)
} else {
let meta = chatItem.meta
let ttl = chat.chatInfo.timedMessagesTTL
let encrypted = chatItem.encryptedFile
switch meta.itemStatus {
case let .sndSent(sndProgress):
switch sndProgress {
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
ZStack {
ciMetaText(
chatItem.meta,
chatTTL: chat.chatInfo.timedMessagesTTL,
encrypted: chatItem.encryptedFile,
color: metaColor,
paleColor: paleMetaColor,
colorMode: invertedMaterial
? .invertedMaterial
: .normal,
showStatus: showStatus,
showEdited: showEdited,
showViaProxy: showSentViaProxy,
showTimesamp: showTimestamp
).invertedForegroundStyle(enabled: invertedMaterial)
if invertedMaterial {
ciMetaText(
chatItem.meta,
chatTTL: chat.chatInfo.timedMessagesTTL,
encrypted: chatItem.encryptedFile,
colorMode: .normal,
onlyOverrides: true,
showStatus: showStatus,
showEdited: showEdited,
showViaProxy: showSentViaProxy,
showTimesamp: showTimestamp
)
}
case let .sndRcvd(_, sndProgress):
switch sndProgress {
case .complete:
ZStack {
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
}
case .partial:
ZStack {
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
}
}
default:
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
}
}
}
}
enum SentCheckmark {
case sent
case rcvd1
case rcvd2
enum MetaColorMode {
// Renders provided colours
case normal
// Fully transparent meta - used for reserving space
case transparent
// Renders white on dark backgrounds and black on light ones
case invertedMaterial
func resolve(_ c: Color?) -> Color? {
switch self {
case .normal: c
case .transparent: .clear
case .invertedMaterial: nil
}
}
func statusSpacer(_ sent: Bool) -> Text {
switch self {
case .normal, .transparent:
Text(
sent
? Image("checkmark.wide")
: Image(systemName: "circlebadge.fill")
).foregroundColor(.clear)
case .invertedMaterial: Text(" ").kerning(13)
}
}
}
func ciMetaText(
_ meta: CIMeta,
chatTTL: Int?,
encrypted: Bool?,
color: Color = .clear,
color: Color = .clear, // we use this function to reserve space without rendering meta
paleColor: Color? = nil,
primaryColor: Color = .accentColor,
transparent: Bool = false,
sent: SentCheckmark? = nil,
colorMode: MetaColorMode = .normal,
onlyOverrides: Bool = false, // only render colors that differ from base
showStatus: Bool = true,
showEdited: Bool = true,
showViaProxy: Bool
showViaProxy: Bool,
showTimesamp: Bool
) -> Text {
var r = Text("")
var space: Text? = nil
let appendSpace = {
if let sp = space {
r = r + sp
space = nil
}
}
let resolved = colorMode.resolve(color)
if showEdited, meta.itemEdited {
r = r + statusIconText("pencil", color)
r = r + statusIconText("pencil", resolved)
}
if meta.disappearing {
r = r + statusIconText("timer", color).font(.caption2)
r = r + statusIconText("timer", resolved).font(.caption2)
let ttl = meta.itemTimed?.ttl
if ttl != chatTTL {
r = r + Text(shortTimeText(ttl)).foregroundColor(color)
r = r + colored(Text(shortTimeText(ttl)), resolved)
}
r = r + Text(" ")
space = Text(" ")
}
if showViaProxy, meta.sentViaProxy == true {
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
appendSpace()
r = r + statusIconText("arrow.forward", resolved?.opacity(0.67)).font(.caption2)
}
if showStatus {
if let (icon, statusColor) = meta.statusIcon(color, primaryColor) {
let t = Text(Image(systemName: icon)).font(.caption2)
let gap = Text(" ").kerning(-1.25)
let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67))
switch sent {
case nil: r = r + t1
case .sent: r = r + t1 + gap
case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) + gap
case .rcvd2: r = r + gap + t1
appendSpace()
if let (image, statusColor) = meta.itemStatus.statusIcon(color, paleColor ?? color, primaryColor) {
let metaColor = if onlyOverrides && statusColor == color {
Color.clear
} else {
colorMode.resolve(statusColor)
}
r = r + Text(" ")
r = r + colored(Text(image), metaColor)
} else if !meta.disappearing {
r = r + statusIconText("circlebadge.fill", .clear) + Text(" ")
r = r + colorMode.statusSpacer(meta.itemStatus.sent)
}
space = Text(" ")
}
if let enc = encrypted {
r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ")
appendSpace()
r = r + statusIconText(enc ? "lock" : "lock.open", resolved)
space = Text(" ")
}
if showTimesamp {
appendSpace()
r = r + colored(meta.timestampText, resolved)
}
r = r + meta.timestampText.foregroundColor(color)
return r.font(.caption)
}
private func statusIconText(_ icon: String, _ color: Color) -> Text {
Text(Image(systemName: icon)).foregroundColor(color)
private func statusIconText(_ icon: String, _ color: Color?) -> Text {
colored(Text(Image(systemName: icon)), color)
}
// Applying `foregroundColor(nil)` breaks `.invertedForegroundStyle` modifier
private func colored(_ t: Text, _ color: Color?) -> Text {
if let color {
t.foregroundColor(color)
} else {
t
}
}
struct CIMetaView_Previews: PreviewProvider {
@@ -15,6 +15,7 @@ struct CIRcvDecryptionError: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
@Environment(\.showTimestamp) var showTimestamp: Bool
var msgDecryptError: MsgDecryptError
var msgCount: UInt32
var chatItem: ChatItem
@@ -47,7 +48,7 @@ struct CIRcvDecryptionError: View {
if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir {
do {
let (member, stats) = try apiGroupMemberInfo(groupInfo.apiId, groupMember.groupMemberId)
let (member, stats) = try apiGroupMemberInfoSync(groupInfo.apiId, groupMember.groupMemberId)
if let s = stats {
m.updateGroupMemberConnectionStats(groupInfo, member, s)
}
@@ -68,37 +69,40 @@ struct CIRcvDecryptionError: View {
}
@ViewBuilder private func viewBody() -> some View {
if case let .direct(contact) = chat.chatInfo,
let contactStats = contact.activeConn?.connectionStats {
if contactStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncContactConnection(contact) }
Group {
if case let .direct(contact) = chat.chatInfo,
let contactStats = contact.activeConn?.connectionStats {
if contactStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncContactConnection(contact) }
}
} else if !contactStats.ratchetSyncSupported {
decryptionErrorItemFixButton(syncSupported: false) {
alert = .syncNotSupportedContactAlert
}
} else {
basicDecryptionErrorItem()
}
} else if !contactStats.ratchetSyncSupported {
decryptionErrorItemFixButton(syncSupported: false) {
alert = .syncNotSupportedContactAlert
} else if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir,
let mem = m.getGroupMember(groupMember.groupMemberId),
let memberStats = mem.wrapped.activeConn?.connectionStats {
if memberStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) }
}
} else if !memberStats.ratchetSyncSupported {
decryptionErrorItemFixButton(syncSupported: false) {
alert = .syncNotSupportedMemberAlert
}
} else {
basicDecryptionErrorItem()
}
} else {
basicDecryptionErrorItem()
}
} else if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir,
let mem = m.getGroupMember(groupMember.groupMemberId),
let memberStats = mem.wrapped.activeConn?.connectionStats {
if memberStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) }
}
} else if !memberStats.ratchetSyncSupported {
decryptionErrorItemFixButton(syncSupported: false) {
alert = .syncNotSupportedMemberAlert
}
} else {
basicDecryptionErrorItem()
}
} else {
basicDecryptionErrorItem()
}
.background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
}
private func basicDecryptionErrorItem() -> some View {
@@ -122,7 +126,7 @@ struct CIRcvDecryptionError: View {
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
}
.padding(.horizontal, 12)
@@ -131,7 +135,6 @@ struct CIRcvDecryptionError: View {
}
.onTapGesture(perform: { onClick() })
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.textSelection(.disabled)
}
@@ -142,7 +145,7 @@ struct CIRcvDecryptionError: View {
.foregroundColor(.red)
.italic()
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
.padding(.horizontal, 12)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
@@ -150,7 +153,6 @@ struct CIRcvDecryptionError: View {
}
.onTapGesture(perform: { onClick() })
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.textSelection(.disabled)
}
@@ -292,30 +292,22 @@ struct CIVideoView: View {
.clipShape(Circle())
}
private func durationProgress() -> some View {
HStack {
Text("\(durationText(videoPlaying ? progress : duration))")
.foregroundColor(.white)
.font(.caption)
.padding(.vertical, 3)
.padding(.horizontal, 6)
.background(Color.black.opacity(0.35))
.cornerRadius(10)
.padding([.top, .leading], 6)
if let file = chatItem.file, !videoPlaying {
Text("\(ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary))")
.foregroundColor(.white)
.font(.caption)
.padding(.vertical, 3)
.padding(.horizontal, 6)
.background(Color.black.opacity(0.35))
.cornerRadius(10)
.padding(.top, 6)
}
private var fileSizeString: String {
if let file = chatItem.file, !videoPlaying {
" " + ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary)
} else {
""
}
}
private func durationProgress() -> some View {
Text((durationText(videoPlaying ? progress : duration)) + fileSizeString)
.invertedForegroundStyle()
.font(.caption)
.padding(.vertical, 6)
.padding(.horizontal, 12)
}
private func imageView(_ img: UIImage) -> some View {
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
return ZStack(alignment: .topTrailing) {
@@ -411,9 +403,9 @@ struct CIVideoView: View {
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
Image(systemName: icon)
.resizable()
.invertedForegroundStyle()
.aspectRatio(contentMode: .fit)
.frame(width: size, height: size)
.foregroundColor(.white)
.padding(smallView ? 0 : padding)
}
@@ -428,10 +420,8 @@ struct CIVideoView: View {
private func progressCircle(_ progress: Int64, _ total: Int64) -> some View {
Circle()
.trim(from: 0, to: Double(progress) / Double(total))
.stroke(
Color(uiColor: .white),
style: StrokeStyle(lineWidth: 2)
)
.stroke(style: StrokeStyle(lineWidth: 2))
.invertedForegroundStyle()
.rotationEffect(.degrees(-90))
.frame(width: 16, height: 16)
.padding([.trailing, .top], smallView ? 0 : 11)
@@ -12,7 +12,7 @@ import SimpleXChat
struct FramedItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
@EnvironmentObject var scrollModel: ReverseListScrollModel
@ObservedObject var chat: Chat
var chatItem: ChatItem
var preview: UIImage?
@@ -64,15 +64,20 @@ struct FramedItemView: View {
.overlay(DetermineWidth())
}
if chatItem.content.msgContent != nil {
CIMetaView(chat: chat, chatItem: chatItem, metaColor: useWhiteMetaColor ? Color.white : theme.colors.secondary)
.padding(.horizontal, 12)
.padding(.bottom, 6)
.overlay(DetermineWidth())
.accessibilityLabel("")
if let content = chatItem.content.msgContent {
CIMetaView(
chat: chat,
chatItem: chatItem,
metaColor: theme.colors.secondary,
invertedMaterial: useWhiteMetaColor
)
.padding(.horizontal, 12)
.padding(.bottom, 6)
.overlay(DetermineWidth())
.accessibilityLabel("")
}
}
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
}
.background { chatItemFrameColorMaybeImageOrVideo(chatItem, theme).modifier(ChatTailPadding()) }
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
if let (title, text) = chatItem.meta.itemStatus.statusInfo {
@@ -185,7 +190,7 @@ struct FramedItemView: View {
let v = ZStack(alignment: .topTrailing) {
switch (qi.content) {
case let .image(_, image):
if let uiImage = UIImage(base64Encoded: image) {
if let uiImage = imageFromBase64(image) {
ciQuotedMsgView(qi)
.padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading)
Image(uiImage: uiImage)
@@ -197,7 +202,7 @@ struct FramedItemView: View {
ciQuotedMsgView(qi)
}
case let .video(_, image, _):
if let uiImage = UIImage(base64Encoded: image) {
if let uiImage = imageFromBase64(image) {
ciQuotedMsgView(qi)
.padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading)
Image(uiImage: uiImage)
@@ -13,7 +13,7 @@ import AVKit
struct FullScreenMediaView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var scrollModel: ReverseListScrollModel<ChatItem>
@EnvironmentObject var scrollModel: ReverseListScrollModel
@State var chatItem: ChatItem
@State var image: UIImage?
@State var player: AVPlayer? = nil
@@ -69,7 +69,7 @@ struct CIMsgError: View {
}
.padding(.leading, 12)
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
.textSelection(.disabled)
.onTapGesture(perform: onTap)
}
@@ -22,7 +22,7 @@ struct MarkedDeletedItemView: View {
.foregroundColor(theme.colors.secondary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(chatItemFrameColor(chatItem, theme))
.background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) }
.textSelection(.disabled)
}
@@ -26,6 +26,7 @@ private func typing(_ w: Font.Weight = .light) -> Text {
struct MsgContentView: View {
@ObservedObject var chat: Chat
@Environment(\.showTimestamp) var showTimestamp: Bool
@EnvironmentObject var theme: AppTheme
var text: String
var formattedText: [FormattedText]? = nil
@@ -84,7 +85,7 @@ struct MsgContentView: View {
}
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
}
@@ -14,12 +14,11 @@ struct ChatItemForwardingView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss
var ci: ChatItem
var chatItems: [ChatItem]
var fromChatInfo: ChatInfo
@Binding var composeState: ComposeState
@State private var searchText: String = ""
@FocusState private var searchFocused
@State private var alert: SomeAlert?
private let chatsToForwardTo = filterChatsToForwardTo(chats: ChatModel.shared.chats)
@@ -46,8 +45,6 @@ struct ChatItemForwardingView: View {
VStack(alignment: .leading) {
if !chatsToForwardTo.isEmpty {
List {
searchFieldView(text: $searchText, focussed: $searchFocused, theme.colors.onBackground, theme.colors.secondary)
.padding(.leading, 2)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { foundChat($0, s) }
ForEach(chats) { chat in
@@ -55,6 +52,7 @@ struct ChatItemForwardingView: View {
.disabled(chatModel.deletedChats.contains(chat.chatInfo.id))
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.modifier(ThemedBackground(grouped: true))
} else {
ZStack {
@@ -73,11 +71,14 @@ struct ChatItemForwardingView: View {
}
@ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View {
let prohibited = chat.prohibitedByPref(
hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text),
isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false,
isVoice: ci.content.msgContent?.isVoice ?? false
)
let prohibited = chatItems.map { ci in
chat.prohibitedByPref(
hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text),
isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false,
isVoice: ci.content.msgContent?.isVoice ?? false
)
}.contains(true)
Button {
if prohibited {
alert = SomeAlert(
@@ -93,10 +94,10 @@ struct ChatItemForwardingView: View {
composeState = ComposeState(
message: composeState.message,
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
contextItem: .forwardingItems(chatItems: chatItems, fromChatInfo: fromChatInfo)
)
} else {
composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
composeState = ComposeState.init(forwardingItems: chatItems, fromChatInfo: fromChatInfo)
ItemsModel.shared.loadOpenChat(chat.id)
}
}
@@ -123,7 +124,7 @@ struct ChatItemForwardingView: View {
#Preview {
ChatItemForwardingView(
ci: ChatItem.getSample(1, .directSnd, .now, "hello"),
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")],
fromChatInfo: .direct(contact: Contact.sampleData),
composeState: Binding.constant(ComposeState(message: "hello"))
).environmentObject(CurrentColors.toAppTheme())
@@ -450,20 +450,8 @@ struct ChatItemInfoView: View {
.foregroundColor(theme.colors.secondary).opacity(0.67)
}
let v = Group {
let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary)
switch status {
case .rcvd:
ZStack(alignment: .trailing) {
Image(systemName: icon)
.foregroundColor(statusColor.opacity(0.67))
.padding(.trailing, 6)
Image(systemName: icon)
.foregroundColor(statusColor.opacity(0.67))
}
default:
Image(systemName: icon)
.foregroundColor(statusColor)
}
let (image, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary)
image.foregroundColor(statusColor)
}
if let (title, text) = status.statusInfo {
+13 -1
View File
@@ -9,9 +9,21 @@
import SwiftUI
import SimpleXChat
extension EnvironmentValues {
struct ShowTimestamp: EnvironmentKey {
static let defaultValue: Bool = true
}
var showTimestamp: Bool {
get { self[ShowTimestamp.self] }
set { self[ShowTimestamp.self] = newValue }
}
}
struct ChatItemView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
@Environment(\.showTimestamp) var showTimestamp: Bool
var chatItem: ChatItem
var maxWidth: CGFloat = .infinity
@Binding var revealed: Bool
@@ -60,7 +72,7 @@ struct ChatItemView: View {
default: nil
}
}
.flatMap { UIImage(base64Encoded: $0) }
.flatMap { imageFromBase64($0) }
let adjustedMaxWidth = {
if let preview, preview.size.width <= preview.size.height {
maxWidth * 0.75
+439 -151
View File
@@ -22,8 +22,7 @@ struct ChatView: View {
@Environment(\.presentationMode) var presentationMode
@Environment(\.scenePhase) var scenePhase
@State @ObservedObject var chat: Chat
@StateObject private var scrollModel = ReverseListScrollModel<ChatItem>()
@StateObject private var floatingButtonModel = FloatingButtonModel()
@StateObject private var scrollModel = ReverseListScrollModel()
@State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false
@State private var composeState = ComposeState()
@@ -43,6 +42,7 @@ struct ChatView: View {
@State private var showGroupLinkSheet: Bool = false
@State private var groupLink: String?
@State private var groupLinkMemberRole: GroupMemberRole = .member
@State private var forwardedChatItems: [ChatItem] = []
@State private var selectedChatItems: Set<Int64>? = nil
@State private var showDeleteSelectedMessages: Bool = false
@State private var allowToDeleteSelectedMessagesForAll: Bool = false
@@ -76,7 +76,7 @@ struct ChatView: View {
VStack(spacing: 0) {
ZStack(alignment: .bottomTrailing) {
chatItemsList()
floatingButtons(counts: floatingButtonModel.unreadChatItemCounts)
FloatingButtons(theme: theme, scrollModel: scrollModel, chat: chat)
}
connectingText()
if selectedChatItems == nil {
@@ -99,7 +99,8 @@ struct ChatView: View {
if case let .group(groupInfo) = chat.chatInfo {
showModerateSelectedMessagesAlert(groupInfo)
}
}
},
forwardItems: forwardSelectedMessages
)
}
}
@@ -136,6 +137,30 @@ struct ChatView: View {
}
}
}
// it should be presented on top level in order to prevent a bug in SwiftUI on iOS 16 related to .focused() modifier in AddGroupMembersView's search field
.appSheet(isPresented: $showAddMembersSheet) {
Group {
if case let .group(groupInfo) = cInfo {
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
}
}
}
.sheet(isPresented: Binding(
get: { !forwardedChatItems.isEmpty },
set: { isPresented in
if !isPresented {
forwardedChatItems = []
selectedChatItems = nil
}
}
)) {
if #available(iOS 16.0, *) {
ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState)
.presentationDetents([.fraction(0.8)])
} else {
ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState)
}
}
.onAppear {
selectedChatItems = nil
initChatView()
@@ -287,9 +312,6 @@ struct ChatView: View {
}
} else {
addMembersButton()
.appSheet(isPresented: $showAddMembersSheet) {
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
}
}
}
Menu {
@@ -340,6 +362,7 @@ struct ChatView: View {
await markChatUnread(chat, unreadChat: false)
}
}
ChatView.FloatingButtonModel.shared.totalUnread = chat.chatStats.unreadCount
}
private func searchToolbar() -> some View {
@@ -411,14 +434,9 @@ struct ChatView: View {
composeState: $composeState,
selectedMember: $selectedMember,
revealedChatItem: $revealedChatItem,
selectedChatItems: $selectedChatItems
selectedChatItems: $selectedChatItems,
forwardedChatItems: $forwardedChatItems
)
.onAppear {
floatingButtonModel.appeared(viewId: ci.viewId)
}
.onDisappear {
floatingButtonModel.disappeared(viewId: ci.viewId)
}
.id(ci.id) // Required to trigger `onAppear` on iOS15
} loadPage: {
loadChatItems(cInfo)
@@ -429,13 +447,10 @@ struct ChatView: View {
.onChange(of: searchText) { _ in
Task { await loadChat(chat: chat, search: searchText) }
}
.onChange(of: im.reversedChatItems) { _ in
floatingButtonModel.chatItemsChanged()
}
.onChange(of: im.itemAdded) { added in
if added {
im.itemAdded = false
if floatingButtonModel.unreadChatItemCounts.isReallyNearBottom {
if FloatingButtonModel.shared.isReallyNearBottom {
scrollModel.scrollToBottom()
}
}
@@ -458,103 +473,169 @@ struct ChatView: View {
}
class FloatingButtonModel: ObservableObject {
private enum Event {
case appeared(String)
case disappeared(String)
case chatItemsChanged
}
static let shared = FloatingButtonModel()
@Published var unreadBelow: Int = 0
@Published var isNearBottom: Bool = true
@Published var date: Date?
@Published var isDateVisible: Bool = false
var totalUnread: Int = 0
var isReallyNearBottom: Bool = true
var hideDateWorkItem: DispatchWorkItem?
@Published var unreadChatItemCounts: UnreadChatItemCounts
private let events = PassthroughSubject<Event, Never>()
private var bag = Set<AnyCancellable>()
init() {
unreadChatItemCounts = UnreadChatItemCounts(
isNearBottom: true,
isReallyNearBottom: true,
unreadBelow: 0
)
events
.receive(on: DispatchQueue.global(qos: .background))
.scan(Set<String>()) { itemsInView, event in
var updated = itemsInView
switch event {
case let .appeared(viewId): updated.insert(viewId)
case let .disappeared(viewId): updated.remove(viewId)
case .chatItemsChanged: ()
}
return updated
func updateOnListChange(_ listState: ListState) {
let im = ItemsModel.shared
let unreadBelow =
if let id = listState.bottomItemId,
let index = im.reversedChatItems.firstIndex(where: { $0.id == id })
{
im.reversedChatItems[..<index].reduce(into: 0) { unread, chatItem in
if chatItem.isRcvNew { unread += 1 }
}
} else {
0
}
.map { ChatModel.shared.unreadChatItemCounts(itemsInView: $0) }
.removeDuplicates()
.throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: true)
.assign(to: \.unreadChatItemCounts, on: self)
.store(in: &bag)
let date: Date? =
if let topItemDate = listState.topItemDate {
Calendar.current.startOfDay(for: topItemDate)
} else {
nil
}
// set the counters and date indicator
DispatchQueue.main.async { [weak self] in
guard let it = self else { return }
it.setDate(visibility: true)
it.unreadBelow = unreadBelow
it.date = date
it.isReallyNearBottom = listState.scrollOffset > 0 && listState.scrollOffset < 500
}
// set floating button indication mode
let nearBottom = listState.scrollOffset < 800
if nearBottom != self.isNearBottom {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in
self?.isNearBottom = nearBottom
}
}
// hide Date indicator after 1 second of no scrolling
hideDateWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let it = self else { return }
it.setDate(visibility: false)
it.hideDateWorkItem = nil
}
DispatchQueue.main.async { [weak self] in
self?.hideDateWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem)
}
}
func appeared(viewId: String) {
events.send(.appeared(viewId))
func resetDate() {
date = nil
isDateVisible = false
}
func disappeared(viewId: String) {
events.send(.disappeared(viewId))
private func setDate(visibility isVisible: Bool) {
if isVisible {
if !isNearBottom,
!isDateVisible,
let date, !Calendar.current.isDateInToday(date) {
withAnimation { self.isDateVisible = true }
}
} else if isDateVisible {
withAnimation { self.isDateVisible = false }
}
}
func chatItemsChanged() {
events.send(.chatItemsChanged)
}
}
private func floatingButtons(counts: UnreadChatItemCounts) -> some View {
VStack {
let unreadAbove = chat.chatStats.unreadCount - counts.unreadBelow
if unreadAbove > 0 {
circleButton {
unreadCountText(unreadAbove)
.font(.callout)
.foregroundColor(theme.colors.primary)
private struct FloatingButtons: View {
let theme: AppTheme
let scrollModel: ReverseListScrollModel
let chat: Chat
@ObservedObject var model = FloatingButtonModel.shared
var body: some View {
ZStack(alignment: .top) {
if let date = model.date {
DateSeparator(date: date)
.padding(.vertical, 4).padding(.horizontal, 8)
.background(.thinMaterial)
.clipShape(Capsule())
.opacity(model.isDateVisible ? 1 : 0)
}
.onTapGesture {
scrollModel.scrollToNextPage()
}
.contextMenu {
Button {
Task {
await markChatRead(chat)
VStack {
let unreadAbove = model.totalUnread - model.unreadBelow
if unreadAbove > 0 {
circleButton {
unreadCountText(unreadAbove)
.font(.callout)
.foregroundColor(theme.colors.primary)
}
} label: {
Label("Mark read", systemImage: "checkmark")
.onTapGesture {
scrollModel.scrollToNextPage()
}
.contextMenu {
Button {
Task {
await markChatRead(chat)
}
} label: {
Label("Mark read", systemImage: "checkmark")
}
}
}
Spacer()
if model.unreadBelow > 0 {
circleButton {
unreadCountText(model.unreadBelow)
.font(.callout)
.foregroundColor(theme.colors.primary)
}
.onTapGesture {
scrollModel.scrollToBottom()
}
} else if !model.isNearBottom {
circleButton {
Image(systemName: "chevron.down")
.foregroundColor(theme.colors.primary)
}
.onTapGesture { scrollModel.scrollToBottom() }
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .trailing)
}
Spacer()
if counts.unreadBelow > 0 {
circleButton {
unreadCountText(counts.unreadBelow)
.font(.callout)
.foregroundColor(theme.colors.primary)
}
.onTapGesture {
scrollModel.scrollToBottom()
}
} else if !counts.isNearBottom {
circleButton {
Image(systemName: "chevron.down")
.foregroundColor(theme.colors.primary)
}
.onTapGesture { scrollModel.scrollToBottom() }
.onDisappear(perform: model.resetDate)
}
private func circleButton<Content: View>(_ content: @escaping () -> Content) -> some View {
ZStack {
Circle()
.foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground))
.frame(width: 44, height: 44)
content()
}
}
.padding()
}
private func circleButton<Content: View>(_ content: @escaping () -> Content) -> some View {
ZStack {
Circle()
.foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground))
.frame(width: 44, height: 44)
content()
private struct DateSeparator: View {
let date: Date
var body: some View {
Text(String.localizedStringWithFormat(
NSLocalizedString("%@, %@", comment: "format for date separator in chat"),
date.formatted(.dateTime.weekday(.abbreviated)),
date.formatted(
Calendar.current.isDate(date, equalTo: .now, toGranularity: .year)
? .dateTime.day().month(.abbreviated)
: .dateTime.day().month(.abbreviated).year()
)
))
.font(.callout)
.fontWeight(.medium)
.foregroundStyle(.secondary)
}
}
@@ -568,8 +649,8 @@ struct ChatView: View {
private func endCallButton(_ call: Call) -> some View {
Button {
if let uuid = call.callkitUUID {
CallController.shared.endCall(callUUID: uuid)
if CallController.useCallKit(), let callUUID = call.callUUID {
CallController.shared.endCall(callUUID: callUUID)
} else {
CallController.shared.endCall(call: call) {}
}
@@ -648,6 +729,116 @@ struct ChatView: View {
}
}
private func forwardSelectedMessages() {
Task {
do {
if let selectedChatItems {
let (validItems, confirmation) = try await apiPlanForwardChatItems(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
itemIds: Array(selectedChatItems)
)
if let confirmation {
if validItems.count > 0 {
showAlert(
String.localizedStringWithFormat(
NSLocalizedString("Forward %d message(s)?", comment: "alert title"),
validItems.count
),
message: forwardConfirmationText(confirmation) + "\n" +
NSLocalizedString("Forward messages without files?", comment: "alert message")
) {
switch confirmation {
case let .filesNotAccepted(fileIds):
[forwardAction(validItems), downloadAction(fileIds), cancelAlertAction]
default:
[forwardAction(validItems), cancelAlertAction]
}
}
} else {
showAlert(
NSLocalizedString("Nothing to forward!", comment: "alert title"),
message: forwardConfirmationText(confirmation)
) {
switch confirmation {
case let .filesNotAccepted(fileIds):
[downloadAction(fileIds), cancelAlertAction]
default:
[okAlertAction]
}
}
}
} else {
await openForwardingSheet(validItems)
}
}
} catch {
logger.error("Plan forward chat items failed: \(error.localizedDescription)")
}
}
func forwardConfirmationText(_ fc: ForwardConfirmation) -> String {
switch fc {
case let .filesNotAccepted(fileIds):
String.localizedStringWithFormat(
NSLocalizedString("%d file(s) were not downloaded.", comment: "forward confirmation reason"),
fileIds.count
)
case let .filesInProgress(filesCount):
String.localizedStringWithFormat(
NSLocalizedString("%d file(s) are still being downloaded.", comment: "forward confirmation reason"),
filesCount
)
case let .filesMissing(filesCount):
String.localizedStringWithFormat(
NSLocalizedString("%d file(s) were deleted.", comment: "forward confirmation reason"),
filesCount
)
case let .filesFailed(filesCount):
String.localizedStringWithFormat(
NSLocalizedString("%d file(s) failed to download.", comment: "forward confirmation reason"),
filesCount
)
}
}
func forwardAction(_ items: [Int64]) -> UIAlertAction {
UIAlertAction(
title: NSLocalizedString("Forward messages", comment: "alert action"),
style: .default,
handler: { _ in Task { await openForwardingSheet(items) } }
)
}
func downloadAction(_ fileIds: [Int64]) -> UIAlertAction {
UIAlertAction(
title: NSLocalizedString("Download files", comment: "alert action"),
style: .default,
handler: { _ in
Task {
if let user = ChatModel.shared.currentUser {
await receiveFiles(user: user, fileIds: fileIds)
}
}
}
)
}
func openForwardingSheet(_ items: [Int64]) async {
let im = ItemsModel.shared
var items = Set(items)
var fci = [ChatItem]()
for reversedChatItem in im.reversedChatItems {
if items.contains(reversedChatItem.id) {
items.remove(reversedChatItem.id)
fci.insert(reversedChatItem, at: 0)
}
if items.isEmpty { break }
}
await MainActor.run { forwardedChatItems = fci }
}
}
private func loadChatItems(_ cInfo: ChatInfo) {
Task {
if loadingItems || firstPage { return }
@@ -696,6 +887,7 @@ struct ChatView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding @ObservedObject var chat: Chat
@ObservedObject var dummyModel: ChatItemDummyModel = .shared
let chatItem: ChatItem
let maxWidth: CGFloat
@Binding var composeState: ComposeState
@@ -708,39 +900,66 @@ struct ChatView: View {
@State private var showDeleteMessages = false
@State private var showChatItemInfoSheet: Bool = false
@State private var chatItemInfo: ChatItemInfo?
@State private var showForwardingSheet: Bool = false
@State private var msgWidth: CGFloat = 0
@Binding var selectedChatItems: Set<Int64>?
@Binding var forwardedChatItems: [ChatItem]
@State private var allowMenu: Bool = true
@State private var markedRead = false
var revealed: Bool { chatItem == revealedChatItem }
typealias ItemSeparation = (timestamp: Bool, largeGap: Bool, date: Date?)
func getItemSeparation(_ chatItem: ChatItem, at i: Int?) -> ItemSeparation {
let im = ItemsModel.shared
if let i, i > 0 && im.reversedChatItems.count >= i {
let nextItem = im.reversedChatItems[i - 1]
let largeGap = !nextItem.chatDir.sameDirection(chatItem.chatDir) || nextItem.meta.itemTs.timeIntervalSince(chatItem.meta.itemTs) > 60
return (
timestamp: largeGap || formatTimestampMeta(chatItem.meta.itemTs) != formatTimestampMeta(nextItem.meta.itemTs),
largeGap: largeGap,
date: Calendar.current.isDate(chatItem.meta.itemTs, inSameDayAs: nextItem.meta.itemTs) ? nil : nextItem.meta.itemTs
)
} else {
return (timestamp: true, largeGap: true, date: nil)
}
}
var body: some View {
let (currIndex, _) = m.getNextChatItem(chatItem)
let currIndex = m.getChatItemIndex(chatItem)
let ciCategory = chatItem.mergeCategory
let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory)
let range = itemsRange(currIndex, prevHidden)
let timeSeparation = getItemSeparation(chatItem, at: currIndex)
let im = ItemsModel.shared
Group {
if revealed, let range = range {
let items = Array(zip(Array(range), im.reversedChatItems[range]))
ForEach(items.reversed(), id: \.1.viewId) { (i, ci) in
let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
chatItemView(ci, nil, prev)
.overlay {
if let selected = selectedChatItems, ci.canBeDeletedForSelf {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
let checked = selected.contains(ci.id)
selectUnselectChatItem(select: !checked, ci)
VStack(spacing: 0) {
ForEach(items.reversed(), id: \.1.viewId) { (i: Int, ci: ChatItem) in
let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
chatItemView(ci, nil, prev, getItemSeparation(ci, at: i))
.overlay {
if let selected = selectedChatItems, ci.canBeDeletedForSelf {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
let checked = selected.contains(ci.id)
selectUnselectChatItem(select: !checked, ci)
}
}
}
}
}
}
} else {
chatItemView(chatItem, range, prevItem)
VStack(spacing: 0) {
chatItemView(chatItem, range, prevItem, timeSeparation)
if let date = timeSeparation.date {
DateSeparator(date: date).padding(8)
}
}
.overlay {
if let selected = selectedChatItems, chatItem.canBeDeletedForSelf {
Color.clear
@@ -754,12 +973,16 @@ struct ChatView: View {
}
}
.onAppear {
if markedRead {
return
} else {
markedRead = true
}
if let range {
if let items = unreadItems(range) {
let itemIds = unreadItemIds(range)
if !itemIds.isEmpty {
waitToMarkRead {
for ci in items {
await apiMarkChatItemRead(chat.chatInfo, ci)
}
await apiMarkChatItemsRead(chat.chatInfo, itemIds)
}
}
} else if chatItem.isRcvNew {
@@ -769,29 +992,75 @@ struct ChatView: View {
}
}
}
private func unreadItems(_ range: ClosedRange<Int>) -> [ChatItem]? {
private func unreadItemIds(_ range: ClosedRange<Int>) -> [ChatItem.ID] {
let im = ItemsModel.shared
let items = range.compactMap { i in
return range.compactMap { i in
if i >= 0 && i < im.reversedChatItems.count {
let ci = im.reversedChatItems[i]
return if ci.isRcvNew { ci } else { nil }
return if ci.isRcvNew { ci.id } else { nil }
} else {
return nil
}
}
return if items.isEmpty { nil } else { items }
}
private func waitToMarkRead(_ op: @Sendable @escaping () async -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
Task {
_ = try? await Task.sleep(nanoseconds: 600_000000)
if m.chatId == chat.chatInfo.id {
Task(operation: op)
await op()
}
}
}
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ prevItem: ChatItem?) -> some View {
@available(iOS 16.0, *)
struct MemberLayout: Layout {
let spacing: Double
let msgWidth: Double
private func sizes(subviews: Subviews, proposal: ProposedViewSize) -> (CGSize, CGSize) {
assert(subviews.count == 2, "member layout must contain exactly two subviews")
let roleSize = subviews[1].sizeThatFits(proposal)
let memberSize = subviews[0].sizeThatFits(
ProposedViewSize(
width: (proposal.width ?? msgWidth) - roleSize.width,
height: proposal.height
)
)
return (memberSize, roleSize)
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
let (memberSize, roleSize) = sizes(subviews: subviews, proposal: proposal)
return CGSize(
width: min(
proposal.width ?? msgWidth,
max(msgWidth, roleSize.width + spacing + memberSize.width)
),
height: max(memberSize.height, roleSize.height)
)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
let (memberSize, roleSize) = sizes(subviews: subviews, proposal: proposal)
subviews[0].place(
at: CGPoint(x: bounds.minX, y: bounds.midY - memberSize.height / 2),
proposal: ProposedViewSize(memberSize)
)
subviews[1].place(
at: CGPoint(
x: bounds.minX + max(memberSize.width + spacing, msgWidth - roleSize.width),
y: bounds.midY - roleSize.height / 2
),
proposal: ProposedViewSize(roleSize)
)
}
}
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ prevItem: ChatItem?, _ itemSeparation: ItemSeparation) -> some View {
let bottomPadding: Double = itemSeparation.largeGap ? 10 : 2
if case let .groupRcv(member) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo {
let (prevMember, memCount): (GroupMember?, Int) =
@@ -803,24 +1072,49 @@ struct ChatView: View {
if prevItem == nil || showMemberImage(member, prevItem) || prevMember != nil {
VStack(alignment: .leading, spacing: 4) {
if ci.content.showMemberName {
let t = if memCount == 1 && member.memberRole > .member {
Text(member.memberRole.text + " ").fontWeight(.semibold) + Text(member.displayName)
} else {
Text(memberNames(member, prevMember, memCount))
Group {
if memCount == 1 && member.memberRole > .member {
Group {
if #available(iOS 16.0, *) {
MemberLayout(spacing: 16, msgWidth: msgWidth) {
Text(member.chatViewName)
.lineLimit(1)
Text(member.memberRole.text)
.fontWeight(.semibold)
.lineLimit(1)
.padding(.trailing, 8)
}
} else {
HStack(spacing: 16) {
Text(member.chatViewName)
.lineLimit(1)
Text(member.memberRole.text)
.fontWeight(.semibold)
.lineLimit(1)
.layoutPriority(1)
}
}
}
.frame(
maxWidth: maxWidth,
alignment: chatItem.chatDir.sent ? .trailing : .leading
)
} else {
Text(memberNames(member, prevMember, memCount))
.lineLimit(2)
}
}
t
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
.padding(.leading, memberImageSize + 14 + (selectedChatItems != nil && ci.canBeDeletedForSelf ? 12 + 24 : 0))
.padding(.top, 7)
.padding(.top, 3) // this is in addition to message sequence gap
}
HStack(alignment: .center, spacing: 0) {
if selectedChatItems != nil && ci.canBeDeletedForSelf {
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
.padding(.trailing, 12)
}
HStack(alignment: .top, spacing: 8) {
HStack(alignment: .top, spacing: 10) {
MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background)
.onTapGesture {
if let member = m.getGroupMember(member.groupMemberId) {
@@ -833,11 +1127,12 @@ struct ChatView: View {
}
}
}
chatItemWithMenu(ci, range, maxWidth)
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
}
}
}
.padding(.bottom, 5)
.padding(.bottom, bottomPadding)
.padding(.trailing)
.padding(.leading, 12)
} else {
@@ -846,11 +1141,11 @@ struct ChatView: View {
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
.padding(.leading, 12)
}
chatItemWithMenu(ci, range, maxWidth)
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
.padding(.trailing)
.padding(.leading, memberImageSize + 8 + 12)
.padding(.leading, 10 + memberImageSize + 12)
}
.padding(.bottom, 5)
.padding(.bottom, bottomPadding)
}
} else {
HStack(alignment: .center, spacing: 0) {
@@ -863,10 +1158,10 @@ struct ChatView: View {
.padding(.leading)
}
}
chatItemWithMenu(ci, range, maxWidth)
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
.padding(.horizontal)
}
.padding(.bottom, 5)
.padding(.bottom, bottomPadding)
}
}
@@ -881,7 +1176,7 @@ struct ChatView: View {
}
}
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat) -> some View {
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat, _ itemSeparation: ItemSeparation) -> some View {
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
VStack(alignment: alignment.horizontal, spacing: 3) {
ChatItemView(
@@ -891,7 +1186,8 @@ struct ChatView: View {
revealed: .constant(revealed),
allowMenu: $allowMenu
)
.modifier(ChatItemClipped(ci))
.environment(\.showTimestamp, itemSeparation.timestamp)
.modifier(ChatItemClipped(ci, tailVisible: itemSeparation.largeGap && (ci.meta.itemDeleted == nil || revealed)))
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
.accessibilityLabel("")
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
@@ -921,14 +1217,6 @@ struct ChatView: View {
}) {
ChatItemInfoView(ci: ci, chatItemInfo: $chatItemInfo)
}
.sheet(isPresented: $showForwardingSheet) {
if #available(iOS 16.0, *) {
ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState)
.presentationDetents([.fraction(0.8)])
} else {
ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState)
}
}
}
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool {
@@ -1069,7 +1357,7 @@ struct ChatView: View {
var forwardButton: Button<some View> {
Button {
showForwardingSheet = true
forwardedChatItems = [chatItem]
} label: {
Label(
NSLocalizedString("Forward", comment: "chat item action"),
@@ -18,7 +18,7 @@ struct ComposeImageView: View {
var body: some View {
HStack(alignment: .center, spacing: 8) {
let imgs: [UIImage] = images.compactMap { image in
UIImage(base64Encoded: image)
imageFromBase64(image)
}
if imgs.count == 0 {
ProgressView()
@@ -40,7 +40,7 @@ struct ComposeLinkView: View {
private func linkPreviewView(_ linkPreview: LinkPreview) -> some View {
HStack(alignment: .center, spacing: 8) {
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
if let uiImage = imageFromBase64(linkPreview.image) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
@@ -23,7 +23,7 @@ enum ComposeContextItem {
case noContextItem
case quotedItem(chatItem: ChatItem)
case editingItem(chatItem: ChatItem)
case forwardingItem(chatItem: ChatItem, fromChatInfo: ChatInfo)
case forwardingItems(chatItems: [ChatItem], fromChatInfo: ChatInfo)
}
enum VoiceMessageRecordingState {
@@ -73,10 +73,10 @@ struct ComposeState {
}
}
init(forwardingItem: ChatItem, fromChatInfo: ChatInfo) {
init(forwardingItems: [ChatItem], fromChatInfo: ChatInfo) {
self.message = ""
self.preview = .noPreview
self.contextItem = .forwardingItem(chatItem: forwardingItem, fromChatInfo: fromChatInfo)
self.contextItem = .forwardingItems(chatItems: forwardingItems, fromChatInfo: fromChatInfo)
self.voiceMessageRecordingState = .noRecording
}
@@ -112,7 +112,7 @@ struct ComposeState {
var forwarding: Bool {
switch contextItem {
case .forwardingItem: return true
case .forwardingItems: return true
default: return false
}
}
@@ -167,6 +167,13 @@ struct ComposeState {
}
}
var manyMediaPreviews: Bool {
switch preview {
case let .mediaPreviews(mediaPreviews): return mediaPreviews.count > 1
default: return false
}
}
var attachmentDisabled: Bool {
if editing || forwarding || liveMessage != nil || inProgress { return true }
switch preview {
@@ -452,7 +459,7 @@ struct ComposeView: View {
Task {
var media: [(String, UploadContent)] = []
for content in selected {
if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
media.append((img, content))
await MainActor.run {
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: media))
@@ -544,7 +551,7 @@ struct ComposeView: View {
}
private func addMediaContent(_ content: UploadContent) async {
if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
var newMedia: [(String, UploadContent?)] = []
if case var .mediaPreviews(media) = composeState.preview {
media.append((img, content))
@@ -687,7 +694,7 @@ struct ComposeView: View {
case let .quotedItem(chatItem: quotedItem):
ContextItemView(
chat: chat,
contextItem: quotedItem,
contextItems: [quotedItem],
contextIcon: "arrowshape.turn.up.left",
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
)
@@ -695,18 +702,17 @@ struct ComposeView: View {
case let .editingItem(chatItem: editingItem):
ContextItemView(
chat: chat,
contextItem: editingItem,
contextItems: [editingItem],
contextIcon: "pencil",
cancelContextItem: { clearState() }
)
Divider()
case let .forwardingItem(chatItem: forwardedItem, _):
case let .forwardingItems(chatItems, _):
ContextItemView(
chat: chat,
contextItem: forwardedItem,
contextItems: chatItems,
contextIcon: "arrowshape.turn.up.forward",
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) },
showSender: false
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
)
Divider()
}
@@ -730,10 +736,11 @@ struct ComposeView: View {
}
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
await sendMemberContactInvitation()
} else if case let .forwardingItem(ci, fromChatInfo) = composeState.contextItem {
sent = await forwardItem(ci, fromChatInfo, ttl)
} else if case let .forwardingItems(chatItems, fromChatInfo) = composeState.contextItem {
// Composed text is send as a reply to the last forwarded item
sent = await forwardItems(chatItems, fromChatInfo, ttl).last
if !composeState.message.isEmpty {
sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl)
_ = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl)
}
} else if case let .editingItem(ci) = composeState.contextItem {
sent = await updateMessage(ci, live: live)
@@ -750,26 +757,28 @@ struct ComposeView: View {
sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl)
case .linkPreview:
sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl)
case let .mediaPreviews(mediaPreviews: media):
case let .mediaPreviews(media):
let last = media.count - 1
var msgs: [ComposedMessage] = []
if last >= 0 {
for i in 0..<last {
if case (_, .video(_, _, _)) = media[i] {
sent = await sendVideo(media[i], ttl: ttl)
} else {
sent = await sendImage(media[i], ttl: ttl)
if i > 0 {
// Sleep to allow `progressByTimeout` update be rendered
try? await Task.sleep(nanoseconds: 100_000000)
}
if let (fileSource, msgContent) = mediaContent(media[i], text: "") {
msgs.append(ComposedMessage(fileSource: fileSource, msgContent: msgContent))
}
_ = try? await Task.sleep(nanoseconds: 100_000000)
}
if case (_, .video(_, _, _)) = media[last] {
sent = await sendVideo(media[last], text: msgText, quoted: quoted, live: live, ttl: ttl)
} else {
sent = await sendImage(media[last], text: msgText, quoted: quoted, live: live, ttl: ttl)
if let (fileSource, msgContent) = mediaContent(media[last], text: msgText) {
msgs.append(ComposedMessage(fileSource: fileSource, quotedItemId: quoted, msgContent: msgContent))
}
}
if sent == nil {
sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl)
if msgs.isEmpty {
msgs = [ComposedMessage(quotedItemId: quoted, msgContent: .text(msgText))]
}
sent = await send(msgs, live: live, ttl: ttl).last
case let .voicePreview(recordingFileName, duration):
stopPlayback.toggle()
let file = voiceCryptoFile(recordingFileName)
@@ -791,6 +800,20 @@ struct ComposeView: View {
}
return sent
func mediaContent(_ media: (String, UploadContent?), text: String) -> (CryptoFile?, MsgContent)? {
let (previewImage, uploadContent) = media
return switch uploadContent {
case let .simpleImage(image):
(saveImage(image), .image(text: text, image: previewImage))
case let .animatedImage(image):
(saveAnimImage(image), .image(text: text, image: previewImage))
case let .video(_, url, duration):
(moveTempFileFromURL(url), .video(text: text, image: previewImage, duration: duration))
case .none:
nil
}
}
func sending() async {
await MainActor.run { composeState.inProgress = true }
}
@@ -854,23 +877,6 @@ struct ComposeView: View {
}
}
func sendImage(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
let (image, data) = imageData
if let data = data, let savedFile = saveAnyImage(data) {
return await send(.image(text: text, image: image), quoted: quoted, file: savedFile, live: live, ttl: ttl)
}
return nil
}
func sendVideo(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
let (image, data) = imageData
if case let .video(_, url, duration) = data, let savedFile = moveTempFileFromURL(url) {
ChatModel.shared.filesToDelete.remove(url)
return await send(.video(text: text, image: image, duration: duration), quoted: quoted, file: savedFile, live: live, ttl: ttl)
}
return nil
}
func voiceCryptoFile(_ fileName: String) -> CryptoFile? {
if !privacyEncryptLocalFilesGroupDefault.get() {
return CryptoFile.plain(fileName)
@@ -887,44 +893,66 @@ struct ComposeView: View {
}
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
if let chatItem = chat.chatInfo.chatType == .local
? await apiCreateChatItem(noteFolderId: chat.chatInfo.apiId, file: file, msg: mc)
: await apiSendMessage(
await send(
[ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc)],
live: live,
ttl: ttl
).first
}
func send(_ msgs: [ComposedMessage], live: Bool, ttl: Int?) async -> [ChatItem] {
if let chatItems = chat.chatInfo.chatType == .local
? await apiCreateChatItems(noteFolderId: chat.chatInfo.apiId, composedMessages: msgs)
: await apiSendMessages(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
file: file,
quotedItemId: quoted,
msg: mc,
live: live,
ttl: ttl
ttl: ttl,
composedMessages: msgs
) {
await MainActor.run {
chatModel.removeLiveDummy(animated: false)
chatModel.addChatItem(chat.chatInfo, chatItem)
for chatItem in chatItems {
chatModel.addChatItem(chat.chatInfo, chatItem)
}
}
return chatItem
return chatItems
}
if let file = file {
removeFile(file.filePath)
for msg in msgs {
if let file = msg.fileSource {
removeFile(file.filePath)
}
}
return nil
return []
}
func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> ChatItem? {
if let chatItem = await apiForwardChatItem(
func forwardItems(_ forwardedItems: [ChatItem], _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> [ChatItem] {
if let chatItems = await apiForwardChatItems(
toChatType: chat.chatInfo.chatType,
toChatId: chat.chatInfo.apiId,
fromChatType: fromChatInfo.chatType,
fromChatId: fromChatInfo.apiId,
itemId: forwardedItem.id,
itemIds: forwardedItems.map { $0.id },
ttl: ttl
) {
await MainActor.run {
chatModel.addChatItem(chat.chatInfo, chatItem)
for chatItem in chatItems {
chatModel.addChatItem(chat.chatInfo, chatItem)
}
if forwardedItems.count != chatItems.count {
showAlert(
String.localizedStringWithFormat(
NSLocalizedString("%d messages not forwarded", comment: "alert title"),
forwardedItems.count - chatItems.count
),
message: NSLocalizedString("Messages were deleted after you selected them.", comment: "alert message")
)
}
}
return chatItem
return chatItems
} else {
return []
}
return nil
}
func checkLinkPreview() -> MsgContent {
@@ -941,14 +969,6 @@ struct ComposeView: View {
return .text(msgText)
}
}
func saveAnyImage(_ img: UploadContent) -> CryptoFile? {
switch img {
case let .simpleImage(image): return saveImage(image)
case let .animatedImage(image): return saveAnimImage(image)
default: return nil
}
}
}
private func startVoiceMessageRecording() async {
@@ -12,7 +12,7 @@ import SimpleXChat
struct ContextItemView: View {
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
let contextItem: ChatItem
let contextItems: [ChatItem]
let contextIcon: String
let cancelContextItem: () -> Void
var showSender: Bool = true
@@ -24,13 +24,22 @@ struct ContextItemView: View {
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundColor(theme.colors.secondary)
if showSender, let sender = contextItem.memberDisplayName {
VStack(alignment: .leading, spacing: 4) {
Text(sender).font(.caption).foregroundColor(theme.colors.secondary)
msgContentView(lines: 2)
}
if let singleItem = contextItems.first, contextItems.count == 1 {
if showSender, let sender = singleItem.memberDisplayName {
VStack(alignment: .leading, spacing: 4) {
Text(sender).font(.caption).foregroundColor(theme.colors.secondary)
msgContentView(lines: 2, contextItem: singleItem)
}
} else {
msgContentView(lines: 3, contextItem: singleItem)
}
} else {
msgContentView(lines: 3)
Text(
chat.chatInfo.chatType == .local
? "Saving \(contextItems.count) messages"
: "Forwarding \(contextItems.count) messages"
)
.italic()
}
Spacer()
Button {
@@ -45,23 +54,32 @@ struct ContextItemView: View {
.padding(12)
.frame(minHeight: 54)
.frame(maxWidth: .infinity)
.background(chatItemFrameColor(contextItem, theme))
.background(background)
}
private func msgContentView(lines: Int) -> some View {
contextMsgPreview()
private var background: Color {
contextItems.first
.map { chatItemFrameColor($0, theme) }
?? Color(uiColor: .tertiarySystemBackground)
}
private func msgContentView(lines: Int, contextItem: ChatItem) -> some View {
contextMsgPreview(contextItem)
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
.lineLimit(lines)
}
private func contextMsgPreview() -> Text {
private func contextMsgPreview(_ contextItem: ChatItem) -> Text {
return attachment() + messageText(contextItem.text, contextItem.formattedText, nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary)
func attachment() -> Text {
let isFileLoaded = if let fileSource = getLoadedFileSource(contextItem.file) {
FileManager.default.fileExists(atPath: getAppFilePath(fileSource.filePath).path)
} else { false }
switch contextItem.content.msgContent {
case .file: return image("doc.fill")
case .file: return isFileLoaded ? image("doc.fill") : Text("")
case .image: return image("photo")
case .voice: return image("play.fill")
case .voice: return isFileLoaded ? image("play.fill") : Text("")
default: return Text("")
}
}
@@ -75,6 +93,6 @@ struct ContextItemView: View {
struct ContextItemView_Previews: PreviewProvider {
static var previews: some View {
let contextItem: ChatItem = ChatItem.getSample(1, .directSnd, .now, "hello")
return ContextItemView(chat: Chat.sampleData, contextItem: contextItem, contextIcon: "pencil.circle", cancelContextItem: {})
return ContextItemView(chat: Chat.sampleData, contextItems: [contextItem], contextIcon: "pencil.circle", cancelContextItem: {})
}
}
@@ -227,6 +227,7 @@ struct SendMessageView: View {
!composeState.editing {
if case .noContextItem = composeState.contextItem,
!composeState.voicePreview,
!composeState.manyMediaPreviews,
let send = sendLiveMessage,
let update = updateLiveMessage {
Button {
@@ -105,8 +105,10 @@ struct AddGroupMembersViewCommon: View {
.padding(.leading, 2)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let members = s == "" ? membersToAdd : membersToAdd.filter { $0.chatViewName.localizedLowercase.contains(s) }
ForEach(members) { contact in
contactCheckView(contact)
ForEach(members + [dummyContact]) { contact in
if contact.contactId != dummyContact.contactId {
contactCheckView(contact)
}
}
}
}
@@ -130,6 +132,14 @@ struct AddGroupMembersViewCommon: View {
.modifier(ThemedBackground(grouped: true))
}
// Resolves keyboard losing focus bug in iOS16 and iOS17,
// when there are no items inside `ForEach(memebers)` loop
private let dummyContact: Contact = {
var dummy = Contact.sampleData
dummy.contactId = -1
return dummy
}()
private func inviteMembersButton() -> some View {
Button {
inviteMembers()
@@ -18,6 +18,7 @@ struct GroupMemberInfoView: View {
var navigation: Bool = false
@State private var connectionStats: ConnectionStats? = nil
@State private var connectionCode: String? = nil
@State private var connectionLoaded: Bool = false
@State private var newRole: GroupMemberRole = .member
@State private var alert: GroupMemberInfoViewAlert?
@State private var sheet: PlanAndConnectActionSheet?
@@ -94,129 +95,137 @@ struct GroupMemberInfoView: View {
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
if member.memberActive {
Section {
if let code = connectionCode { verifyCodeButton(code) }
if let connStats = connectionStats,
connStats.ratchetSyncAllowed {
synchronizeConnectionButton()
}
// } else if developerTools {
// synchronizeConnectionButtonForce()
// }
}
}
if connectionLoaded {
if let contactLink = member.contactLink {
Section {
SimpleXLinkQRCode(uri: contactLink)
Button {
showShareSheet(items: [simplexChatLink(contactLink)])
} label: {
Label("Share address", systemImage: "square.and.arrow.up")
if member.memberActive {
Section {
if let code = connectionCode { verifyCodeButton(code) }
if let connStats = connectionStats,
connStats.ratchetSyncAllowed {
synchronizeConnectionButton()
}
// } else if developerTools {
// synchronizeConnectionButtonForce()
// }
}
if let contactId = member.memberContactId {
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
}
if let contactLink = member.contactLink {
Section {
SimpleXLinkQRCode(uri: contactLink)
Button {
showShareSheet(items: [simplexChatLink(contactLink)])
} label: {
Label("Share address", systemImage: "square.and.arrow.up")
}
if let contactId = member.memberContactId {
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
connectViaAddressButton(contactLink)
}
} else {
connectViaAddressButton(contactLink)
}
} else {
connectViaAddressButton(contactLink)
} header: {
Text("Address")
.foregroundColor(theme.colors.secondary)
} footer: {
Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.")
.foregroundColor(theme.colors.secondary)
}
} header: {
Text("Address")
.foregroundColor(theme.colors.secondary)
} footer: {
Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.")
.foregroundColor(theme.colors.secondary)
}
}
Section(header: Text("Member").foregroundColor(theme.colors.secondary)) {
infoRow("Group", groupInfo.displayName)
Section(header: Text("Member").foregroundColor(theme.colors.secondary)) {
infoRow("Group", groupInfo.displayName)
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
Picker("Change role", selection: $newRole) {
ForEach(roles) { role in
Text(role.text)
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
Picker("Change role", selection: $newRole) {
ForEach(roles) { role in
Text(role.text)
}
}
.frame(height: 36)
} else {
infoRow("Role", member.memberRole.text)
}
.frame(height: 36)
} else {
infoRow("Role", member.memberRole.text)
}
}
if let connStats = connectionStats {
Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) {
// TODO network connection status
Button("Change receiving address") {
alert = .switchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|| connStats.ratchetSyncSendProhibited
)
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
Button("Abort changing address") {
alert = .abortSwitchAddressAlert
if let connStats = connectionStats {
Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) {
// TODO network connection status
Button("Change receiving address") {
alert = .switchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|| connStats.ratchetSyncSendProhibited
)
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
Button("Abort changing address") {
alert = .abortSwitchAddressAlert
}
.disabled(
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch }
|| connStats.ratchetSyncSendProhibited
)
}
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary)
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary)
}
smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary)
smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary)
}
}
if groupInfo.membership.memberRole >= .admin {
adminDestructiveSection(member)
} else {
nonAdminBlockSection(member)
}
if groupInfo.membership.memberRole >= .admin {
adminDestructiveSection(member)
} else {
nonAdminBlockSection(member)
}
if developerTools {
Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
infoRow("Local name", member.localDisplayName)
infoRow("Database ID", "\(member.groupMemberId)")
if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
infoRow("Connection", connLevelDesc)
}
Button ("Debug delivery") {
Task {
do {
let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId))
await MainActor.run { alert = .queueInfo(info: info) }
} catch let e {
logger.error("apiContactQueueInfo error: \(responseError(e))")
let a = getErrorAlert(e, "Error")
await MainActor.run { alert = .error(title: a.title, error: a.message) }
if developerTools {
Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
infoRow("Local name", member.localDisplayName)
infoRow("Database ID", "\(member.groupMemberId)")
if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
infoRow("Connection", connLevelDesc)
}
Button ("Debug delivery") {
Task {
do {
let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId))
await MainActor.run { alert = .queueInfo(info: info) }
} catch let e {
logger.error("apiContactQueueInfo error: \(responseError(e))")
let a = getErrorAlert(e, "Error")
await MainActor.run { alert = .error(title: a.title, error: a.message) }
}
}
}
}
}
}
}
.navigationBarHidden(true)
.onAppear {
.task {
if #unavailable(iOS 16) {
// this condition prevents re-setting picker
if !justOpened { return }
}
justOpened = false
DispatchQueue.main.async {
newRole = member.memberRole
do {
let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
newRole = member.memberRole
do {
let (_, stats) = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
let (mem, code) = member.memberActive ? try await apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil)
await MainActor.run {
_ = chatModel.upsertGroupMember(groupInfo, mem)
connectionStats = stats
connectionCode = code
} catch let error {
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
connectionLoaded = true
}
} catch let error {
await MainActor.run {
connectionLoaded = true
}
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
}
}
.onChange(of: newRole) { newRole in
@@ -110,10 +110,13 @@ struct GroupProfileView: View {
}
}
.onChange(of: chosenImage) { image in
if let image = image {
groupProfile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
} else {
groupProfile.image = nil
Task {
let resized: String? = if let image {
await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
} else {
nil
}
await MainActor.run { groupProfile.image = resized }
}
}
.onAppear {
+74 -12
View File
@@ -8,15 +8,16 @@
import SwiftUI
import Combine
import SimpleXChat
/// A List, which displays it's items in reverse order - from bottom to top
struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIViewControllerRepresentable {
let items: Array<Item>
struct ReverseList<Content: View>: UIViewControllerRepresentable {
let items: Array<ChatItem>
@Binding var scrollState: ReverseListScrollModel<Item>.State
@Binding var scrollState: ReverseListScrollModel.State
/// Closure, that returns user interface for a given item
let content: (Item) -> Content
let content: (ChatItem) -> Content
let loadPage: () -> Void
@@ -25,7 +26,9 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
}
func updateUIViewController(_ controller: Controller, context: Context) {
controller.representer = self
if case let .scrollingTo(destination) = scrollState, !items.isEmpty {
controller.view.layer.removeAllAnimations()
switch destination {
case .nextPage:
controller.scrollToNextPage()
@@ -42,9 +45,10 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
/// Controller, which hosts SwiftUI cells
class Controller: UITableViewController {
private enum Section { case main }
private let representer: ReverseList
private var dataSource: UITableViewDiffableDataSource<Section, Item>!
var representer: ReverseList
private var dataSource: UITableViewDiffableDataSource<Section, ChatItem>!
private var itemCount: Int = 0
private let updateFloatingButtons = PassthroughSubject<Void, Never>()
private var bag = Set<AnyCancellable>()
init(representer: ReverseList) {
@@ -71,7 +75,7 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
}
// 3. Configure data source
self.dataSource = UITableViewDiffableDataSource<Section, Item>(
self.dataSource = UITableViewDiffableDataSource<Section, ChatItem>(
tableView: tableView
) { (tableView, indexPath, item) -> UITableViewCell? in
if indexPath.item > self.itemCount - 8, self.itemCount > 8 {
@@ -103,6 +107,15 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
name: notificationName,
object: nil
)
updateFloatingButtons
.throttle(for: 0.2, scheduler: DispatchQueue.global(qos: .background), latest: true)
.sink {
if let listState = DispatchQueue.main.sync(execute: { [weak self] in self?.getListState() }) {
ChatView.FloatingButtonModel.shared.updateOnListChange(listState)
}
}
.store(in: &bag)
}
@available(*, unavailable)
@@ -171,8 +184,8 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
Task { representer.scrollState = .atDestination }
}
func update(items: Array<Item>) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
func update(items: [ChatItem]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, ChatItem>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
dataSource.defaultRowAnimation = .none
@@ -180,7 +193,50 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
snapshot,
animatingDifferences: itemCount != 0 && abs(items.count - itemCount) == 1
)
// Sets content offset on initial load
if itemCount == 0 {
tableView.setContentOffset(
CGPoint(x: 0, y: -InvertedTableView.inset),
animated: false
)
}
itemCount = items.count
updateFloatingButtons.send()
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateFloatingButtons.send()
}
func getListState() -> ListState? {
if let visibleRows = tableView.indexPathsForVisibleRows,
visibleRows.last?.item ?? 0 < representer.items.count {
let scrollOffset: Double = tableView.contentOffset.y + InvertedTableView.inset
let topItemDate: Date? =
if let lastVisible = visibleRows.last(where: { isVisible(indexPath: $0) }) {
representer.items[lastVisible.item].meta.itemTs
} else {
nil
}
let bottomItemId: ChatItem.ID? =
if let firstVisible = visibleRows.first(where: { isVisible(indexPath: $0) }) {
representer.items[firstVisible.item].id
} else {
nil
}
return (scrollOffset: scrollOffset, topItemDate: topItemDate, bottomItemId: bottomItemId)
}
return nil
}
private func isVisible(indexPath: IndexPath) -> Bool {
if let relativeFrame = tableView.superview?.convert(
tableView.rectForRow(at: indexPath),
from: tableView
) {
relativeFrame.maxY > InvertedTableView.inset &&
relativeFrame.minY < tableView.frame.height - InvertedTableView.inset
} else { false }
}
}
@@ -224,13 +280,19 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
}
}
typealias ListState = (
scrollOffset: Double,
topItemDate: Date?,
bottomItemId: ChatItem.ID?
)
/// Manages ``ReverseList`` scrolling
class ReverseListScrollModel<Item: Identifiable>: ObservableObject {
class ReverseListScrollModel: ObservableObject {
/// Represents Scroll State of ``ReverseList``
enum State: Equatable {
enum Destination: Equatable {
case nextPage
case item(Item.ID)
case item(ChatItem.ID)
case bottom
}
@@ -248,7 +310,7 @@ class ReverseListScrollModel<Item: Identifiable>: ObservableObject {
state = .scrollingTo(.bottom)
}
func scrollToItem(id: Item.ID) {
func scrollToItem(id: ChatItem.ID) {
state = .scrollingTo(.item(id))
}
}
@@ -32,13 +32,17 @@ struct SelectedItemsBottomToolbar: View {
var deleteItems: (Bool) -> Void
var moderateItems: () -> Void
//var shareItems: () -> Void
var forwardItems: () -> Void
@State var deleteEnabled: Bool = false
@State var deleteForEveryoneEnabled: Bool = false
@State var canModerate: Bool = false
@State var moderateEnabled: Bool = false
@State var allButtonsDisabled = false
@State var forwardEnabled: Bool = false
@State var deleteCountProhibited = false
@State var forwardCountProhibited = false
var body: some View {
VStack(spacing: 0) {
@@ -50,10 +54,11 @@ struct SelectedItemsBottomToolbar: View {
} label: {
Image(systemName: "trash")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20, alignment: .center)
.foregroundColor(!deleteEnabled || allButtonsDisabled ? theme.colors.secondary: .red)
.foregroundColor(!deleteEnabled || deleteCountProhibited ? theme.colors.secondary: .red)
}
.disabled(!deleteEnabled || allButtonsDisabled)
.disabled(!deleteEnabled || deleteCountProhibited)
Spacer()
Button {
@@ -61,24 +66,24 @@ struct SelectedItemsBottomToolbar: View {
} label: {
Image(systemName: "flag")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20, alignment: .center)
.foregroundColor(!moderateEnabled || allButtonsDisabled ? theme.colors.secondary : .red)
.foregroundColor(!moderateEnabled || deleteCountProhibited ? theme.colors.secondary : .red)
}
.disabled(!moderateEnabled || allButtonsDisabled)
.disabled(!moderateEnabled || deleteCountProhibited)
.opacity(canModerate ? 1 : 0)
Spacer()
Button {
//shareItems()
forwardItems()
} label: {
Image(systemName: "square.and.arrow.up")
Image(systemName: "arrowshape.turn.up.forward")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20, alignment: .center)
.foregroundColor(allButtonsDisabled ? theme.colors.secondary : theme.colors.primary)
.foregroundColor(!forwardEnabled || forwardCountProhibited ? theme.colors.secondary : theme.colors.primary)
}
.disabled(allButtonsDisabled)
.opacity(0)
.disabled(!forwardEnabled || forwardCountProhibited)
}
.frame(maxHeight: .infinity)
.padding([.leading, .trailing], 12)
@@ -101,20 +106,22 @@ struct SelectedItemsBottomToolbar: View {
private func recheckItems(_ chatInfo: ChatInfo, _ chatItems: [ChatItem], _ selectedItems: Set<Int64>?) {
let count = selectedItems?.count ?? 0
allButtonsDisabled = count == 0 || count > 20
deleteCountProhibited = count == 0 || count > 200
forwardCountProhibited = count == 0 || count > 20
canModerate = possibleToModerate(chatInfo)
if let selected = selectedItems {
let me: Bool
let onlyOwnGroupItems: Bool
(deleteEnabled, deleteForEveryoneEnabled, me, onlyOwnGroupItems, selectedChatItems) = chatItems.reduce((true, true, true, true, [])) { (r, ci) in
(deleteEnabled, deleteForEveryoneEnabled, me, onlyOwnGroupItems, forwardEnabled, selectedChatItems) = chatItems.reduce((true, true, true, true, true, [])) { (r, ci) in
if selected.contains(ci.id) {
var (de, dee, me, onlyOwnGroupItems, sel) = r
var (de, dee, me, onlyOwnGroupItems, fe, sel) = r
de = de && ci.canBeDeletedForSelf
dee = dee && ci.meta.deletable && !ci.localNote
onlyOwnGroupItems = onlyOwnGroupItems && ci.chatDir == .groupSnd
me = me && ci.content.msgContent != nil && ci.memberToModerate(chatInfo) != nil
fe = fe && ci.content.msgContent != nil && ci.meta.itemDeleted == nil && !ci.isLiveDummy
sel.insert(ci.id) // we are collecting new selected items here to account for any changes in chat items list
return (de, dee, me, onlyOwnGroupItems, sel)
return (de, dee, me, onlyOwnGroupItems, fe, sel)
} else {
return r
}
@@ -10,7 +10,7 @@ import SwiftUI
struct ChatHelp: View {
@EnvironmentObject var chatModel: ChatModel
@Binding var showSettings: Bool
let dismissSettingsSheet: DismissAction
var body: some View {
ScrollView { chatHelp() }
@@ -23,7 +23,7 @@ struct ChatHelp: View {
VStack(alignment: .leading, spacing: 0) {
Text("To ask any questions and to receive updates:")
Button("connect to SimpleX Chat developers.") {
showSettings = false
dismissSettingsSheet()
DispatchQueue.main.async {
UIApplication.shared.open(simplexTeamURL)
}
@@ -61,8 +61,9 @@ struct ChatHelp: View {
}
struct ChatHelp_Previews: PreviewProvider {
@Environment(\.dismiss) static var mockDismiss
static var previews: some View {
@State var showSettings = false
return ChatHelp(showSettings: $showSettings)
ChatHelp(dismissSettingsSheet: mockDismiss)
}
}
+121 -39
View File
@@ -9,18 +9,87 @@
import SwiftUI
import SimpleXChat
enum UserPickerSheet: Identifiable {
case address
case chatPreferences
case chatProfiles
case currentProfile
case useFromDesktop
case settings
var id: Self { self }
var navigationTitle: LocalizedStringKey {
switch self {
case .address: "SimpleX address"
case .chatPreferences: "Your preferences"
case .chatProfiles: "Your chat profiles"
case .currentProfile: "Your current profile"
case .useFromDesktop: "Connect to desktop"
case .settings: "Your settings"
}
}
}
struct UserPickerSheetView: View {
let sheet: UserPickerSheet
@EnvironmentObject var chatModel: ChatModel
@State private var loaded = false
var body: some View {
NavigationView {
ZStack {
if loaded, let currentUser = chatModel.currentUser {
switch sheet {
case .address:
UserAddressView(shareViaProfile: currentUser.addressShared)
case .chatPreferences:
PreferencesView(
profile: currentUser.profile,
preferences: currentUser.fullPreferences,
currentPreferences: currentUser.fullPreferences
)
case .chatProfiles:
UserProfilesView()
case .currentProfile:
UserProfile()
case .useFromDesktop:
ConnectDesktopView()
case .settings:
SettingsView()
}
}
Color.clear // Required for list background to be rendered during loading
}
.navigationTitle(sheet.navigationTitle)
.navigationBarTitleDisplayMode(.large)
.modifier(ThemedBackground(grouped: true))
}
.overlay {
if let la = chatModel.laRequest {
LocalAuthView(authRequest: la)
}
}
.task {
withAnimation(
.easeOut(duration: 0.1),
{ loaded = true }
)
}
}
}
struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool
@Binding var activeUserPickerSheet: UserPickerSheet?
@State private var searchMode = false
@FocusState private var searchFocussed
@State private var searchText = ""
@State private var searchShowingSimplexLink = false
@State private var searchChatFilteredBySimplexLink: String? = nil
@State private var userPickerVisible = false
@State private var showConnectDesktop = false
@State private var scrollToSearchBar = false
@State private var userPickerShown: Bool = false
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
@@ -46,21 +115,21 @@ struct ChatListView: View {
),
destination: chatView
) { chatListView }
if userPickerVisible {
Rectangle().fill(.white.opacity(0.001)).onTapGesture {
withAnimation {
userPickerVisible.toggle()
}
}
.modifier(
Sheet(isPresented: $userPickerShown) {
UserPicker(userPickerShown: $userPickerShown, activeSheet: $activeUserPickerSheet)
}
)
.sheet(item: $activeUserPickerSheet) {
UserPickerSheetView(sheet: $0)
}
.onChange(of: activeUserPickerSheet) {
if $0 != nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
userPickerShown = false
}
}
UserPicker(
showSettings: $showSettings,
showConnectDesktop: $showConnectDesktop,
userPickerVisible: $userPickerVisible
)
}
.sheet(isPresented: $showConnectDesktop) {
ConnectDesktopView()
}
}
@@ -73,7 +142,7 @@ struct ChatListView: View {
.navigationBarHidden(searchMode || oneHandUI)
}
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
.onDisappear() { withAnimation { userPickerVisible = false } }
.onDisappear() { activeUserPickerSheet = nil }
.refreshable {
AlertManager.shared.showAlert(Alert(
title: Text("Reconnect servers?"),
@@ -164,7 +233,7 @@ struct ChatListView: View {
let user = chatModel.currentUser ?? User.sampleData
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel))
.padding(.trailing, 4)
.padding([.top, .trailing], 3)
let allRead = chatModel.users
.filter { u in !u.user.activeUser && !u.user.hidden }
.allSatisfy { u in u.unreadCount == 0 }
@@ -173,13 +242,7 @@ struct ChatListView: View {
}
}
.onTapGesture {
if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 {
withAnimation {
userPickerVisible.toggle()
}
} else {
showSettings = true
}
userPickerShown = true
}
}
@@ -217,14 +280,31 @@ struct ChatListView: View {
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
ForEach(cs, id: \.viewId) { chat in
ChatListNavLink(chat: chat)
if #available(iOS 16.0, *) {
ForEach(cs, id: \.viewId) { chat in
ChatListNavLink(chat: chat)
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
.padding(.trailing, -16)
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
.listRowBackground(Color.clear)
}
.offset(x: -8)
} else {
ForEach(cs, id: \.viewId) { chat in
VStack(spacing: .zero) {
Divider()
.padding(.leading, 16)
ChatListNavLink(chat: chat)
.padding(.horizontal, 8)
.padding(.vertical, 6)
}
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
.padding(.trailing, -16)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets())
.background { theme.colors.background } // Hides default list selection colour
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
.listRowBackground(Color.clear)
}
}
.offset(x: -8)
}
.listStyle(.plain)
.onChange(of: chatModel.chatId) { currentChatId in
@@ -252,7 +332,7 @@ struct ChatListView: View {
}
}
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
private func unreadBadge(size: CGFloat = 18) -> some View {
Circle()
.frame(width: size, height: size)
.foregroundColor(theme.colors.primary)
@@ -357,7 +437,7 @@ struct SubsStatusIndicator: View {
private func startTask() {
task = Task {
while !Task.isCancelled {
if AppChatState.shared.value == .active {
if AppChatState.shared.value == .active, ChatModel.shared.chatRunning == true {
do {
let (subs, hasSess) = try await getAgentSubsTotal()
await MainActor.run {
@@ -499,27 +579,29 @@ func chatStoppedIcon() -> some View {
}
struct ChatListView_Previews: PreviewProvider {
@State static var userPickerSheet: UserPickerSheet? = .none
static var previews: some View {
let chatModel = ChatModel()
chatModel.chats = [
Chat(
chatModel.updateChats([
ChatData(
chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
),
Chat(
ChatData(
chatInfo: ChatInfo.sampleData.group,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "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.")]
),
Chat(
ChatData(
chatInfo: ChatInfo.sampleData.contactRequest,
chatItems: []
)
]
])
return Group {
ChatListView(showSettings: Binding.constant(false))
ChatListView(activeUserPickerSheet: $userPickerSheet)
.environmentObject(chatModel)
ChatListView(showSettings: Binding.constant(false))
ChatListView(activeUserPickerSheet: $userPickerSheet)
.environmentObject(ChatModel())
}
}
@@ -35,11 +35,16 @@ struct ChatPreviewView: View {
}
.padding(.leading, 4)
let chatTs = if let cItem {
cItem.meta.itemTs
} else {
chat.chatInfo.chatTs
}
VStack(spacing: 0) {
HStack(alignment: .top) {
chatPreviewTitle()
Spacer()
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs))
(formatTimestampText(chatTs))
.font(.subheadline)
.frame(minWidth: 60, alignment: .trailing)
.foregroundColor(theme.colors.secondary)
@@ -302,7 +307,7 @@ struct ChatPreviewView: View {
case let .link(_, preview):
smallContentPreview(size: dynamicMediaSize) {
ZStack(alignment: .topTrailing) {
Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!)
Image(uiImage: imageFromBase64(preview.image) ?? UIImage(systemName: "arrow.up.right")!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: dynamicMediaSize, height: dynamicMediaSize)
@@ -323,13 +328,13 @@ struct ChatPreviewView: View {
}
case let .image(_, image):
smallContentPreview(size: dynamicMediaSize) {
CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
.environmentObject(ReverseListScrollModel<ChatItem>())
CIImageView(chatItem: ci, preview: imageFromBase64(image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
.environmentObject(ReverseListScrollModel())
}
case let .video(_,image, duration):
smallContentPreview(size: dynamicMediaSize) {
CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
.environmentObject(ReverseListScrollModel<ChatItem>())
CIVideoView(chatItem: ci, preview: imageFromBase64(image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
.environmentObject(ReverseListScrollModel())
}
case let .voice(_, duration):
smallContentPreviewVoice(size: dynamicMediaSize) {
@@ -407,12 +407,18 @@ struct ServersSummaryView: View {
struct SubscriptionStatusIndicatorView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
var subs: SMPServerSubs
var hasSess: Bool
var body: some View {
let onionHosts = networkUseOnionHostsGroupDefault.get()
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess)
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(
online: m.networkInfo.online,
usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil,
subs: subs,
hasSess: hasSess,
primaryColor: theme.colors.primary
)
if #available(iOS 16.0, *) {
Image(systemName: "dot.radiowaves.up.forward", variableValue: variableValue)
.foregroundColor(color)
@@ -425,26 +431,32 @@ struct SubscriptionStatusIndicatorView: View {
struct SubscriptionStatusPercentageView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
var subs: SMPServerSubs
var hasSess: Bool
var body: some View {
let onionHosts = networkUseOnionHostsGroupDefault.get()
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess)
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(
online: m.networkInfo.online,
usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil,
subs: subs,
hasSess: hasSess,
primaryColor: theme.colors.primary
)
Text(verbatim: "\(Int(floor(statusPercent * 100)))%")
.foregroundColor(.secondary)
.font(.caption)
}
}
func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ hasSess: Bool) -> (Color, Double, Double, Double) {
func subscriptionStatusColorAndPercentage(online: Bool, usesProxy: Bool, subs: SMPServerSubs, hasSess: Bool, primaryColor: Color) -> (Color, Double, Double, Double) {
func roundedToQuarter(_ n: Double) -> Double {
n >= 1 ? 1
: n <= 0 ? 0
: (n * 4).rounded() / 4
}
let activeColor: Color = onionHosts == .require ? .indigo : .accentColor
let activeColor: Color = usesProxy ? .indigo : primaryColor
let noConnColorAndPercent: (Color, Double, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1, 0)
let activeSubsRounded = roundedToQuarter(subs.shareOfActive)
+237 -138
View File
@@ -8,176 +8,275 @@ import SimpleXChat
struct UserPicker: View {
@EnvironmentObject var m: ChatModel
@Environment(\.scenePhase) var scenePhase
@EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool
@Binding var showConnectDesktop: Bool
@Binding var userPickerVisible: Bool
@State var scrollViewContentSize: CGSize = .zero
@State var disableScrolling: Bool = true
private let menuButtonHeight: CGFloat = 68
@State var chatViewNameWidth: CGFloat = 0
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
@Environment(\.scenePhase) private var scenePhase: ScenePhase
@Environment(\.colorScheme) private var colorScheme: ColorScheme
@Binding var userPickerShown: Bool
@Binding var activeSheet: UserPickerSheet?
@State private var currentUser: Int64?
@State private var switchingProfile = false
@State private var frameWidth: CGFloat = 0
@State private var resetScroll = ResetScrollAction()
// Inset grouped list dimensions
private let imageSize: CGFloat = 44
private let rowPadding: CGFloat = 16
private let rowVerticalPadding: CGFloat = 11
private let sectionSpacing: CGFloat = 35
private var sectionHorizontalPadding: CGFloat { frameWidth > 375 ? 20 : 16 }
private let sectionShape = RoundedRectangle(cornerRadius: 10, style: .continuous)
var body: some View {
VStack {
Spacer().frame(height: 1)
let otherUsers: [UserInfo] = m.users
.filter { u in !u.user.hidden && u.user.userId != m.currentUser?.userId }
.sorted(using: KeyPathComparator<UserInfo>(\.user.activeOrder, order: .reverse))
let sectionWidth = max(frameWidth - sectionHorizontalPadding * 2, 0)
let currentUserWidth = max(frameWidth - sectionHorizontalPadding - rowPadding * 2 - 14 - imageSize, 0)
let stopped = m.chatRunning != true
VStack(spacing: sectionSpacing) {
if let user = m.currentUser {
StickyScrollView(resetScroll: $resetScroll) {
HStack(spacing: rowPadding) {
HStack {
ProfileImage(imageStr: user.image, size: imageSize, color: Color(uiColor: .tertiarySystemGroupedBackground))
.padding(.trailing, 6)
profileName(user).lineLimit(1)
}
.padding(rowPadding)
.frame(width: otherUsers.isEmpty ? sectionWidth : currentUserWidth, alignment: .leading)
.modifier(ListRow { activeSheet = .currentProfile })
.clipShape(sectionShape)
.disabled(stopped)
.opacity(stopped ? 0.4 : 1)
ForEach(otherUsers) { u in
userView(u, size: imageSize)
.frame(maxWidth: sectionWidth * 0.618)
.fixedSize()
.disabled(stopped)
.opacity(stopped ? 0.4 : 1)
}
}
.padding(.horizontal, sectionHorizontalPadding)
}
.frame(height: 2 * rowPadding + imageSize)
.padding(.top, sectionSpacing)
.overlay(DetermineWidth())
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
}
VStack(spacing: 0) {
ScrollView {
ScrollViewReader { sp in
let users = m.users
.filter({ u in u.user.activeUser || !u.user.hidden })
.sorted { u, _ in u.user.activeUser }
VStack(spacing: 0) {
ForEach(users) { u in
userView(u)
Divider()
if u.user.activeUser { Divider() }
openSheetOnTap("qrcode", title: m.userAddress == nil ? "Create SimpleX address" : "Your SimpleX address", sheet: .address, disabled: stopped)
openSheetOnTap("switch.2", title: "Chat preferences", sheet: .chatPreferences, disabled: stopped)
openSheetOnTap("person.crop.rectangle.stack", title: "Your chat profiles", sheet: .chatProfiles, disabled: stopped)
openSheetOnTap("desktopcomputer", title: "Use from desktop", sheet: .useFromDesktop, disabled: stopped)
ZStack(alignment: .trailing) {
openSheetOnTap("gearshape", title: "Settings", sheet: .settings, showDivider: false)
Image(systemName: colorScheme == .light ? "sun.max" : "moon.fill")
.resizable()
.scaledToFit()
.symbolRenderingMode(.monochrome)
.foregroundColor(theme.colors.secondary)
.frame(maxWidth: 20, maxHeight: .infinity)
.padding(.horizontal, rowPadding)
.background(Color(.systemBackground).opacity(0.01))
.onTapGesture {
if (colorScheme == .light) {
ThemeManager.applyTheme(systemDarkThemeDefault.get())
} else {
ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName)
}
}
.overlay {
GeometryReader { geo -> Color in
DispatchQueue.main.async {
scrollViewContentSize = geo.size
let scenes = UIApplication.shared.connectedScenes
if let windowScene = scenes.first as? UIWindowScene {
let layoutFrame = windowScene.windows[0].safeAreaLayoutGuide.layoutFrame
disableScrolling = scrollViewContentSize.height + menuButtonHeight + 10 < layoutFrame.height
}
}
return Color.clear
}
.onLongPressGesture {
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
}
.onChange(of: userPickerVisible) { visible in
if visible, let u = users.first {
sp.scrollTo(u.id)
}
}
}
}
.simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000))
.frame(maxHeight: scrollViewContentSize.height)
menuButton("Use from desktop", icon: "desktopcomputer") {
showConnectDesktop = true
withAnimation {
userPickerVisible.toggle()
}
}
Divider()
menuButton("Settings", icon: "gearshape") {
showSettings = true
withAnimation {
userPickerVisible.toggle()
}
}
}
.clipShape(sectionShape)
.padding(.horizontal, sectionHorizontalPadding)
.padding(.bottom, sectionSpacing)
}
.clipShape(RoundedRectangle(cornerRadius: 16))
.background(
Rectangle()
.fill(theme.colors.surface)
.cornerRadius(16)
.shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0)
)
.onPreferenceChange(DetermineWidth.Key.self) { chatViewNameWidth = $0 }
.frame(maxWidth: chatViewNameWidth > 0 ? min(300, chatViewNameWidth + 130) : 300)
.padding(8)
.opacity(userPickerVisible ? 1.0 : 0.0)
.onAppear {
do {
// This check prevents the call of listUsers after the app is suspended, and the database is closed.
if case .active = scenePhase {
m.users = try listUsers()
}
} catch let error {
logger.error("Error loading users \(responseError(error))")
}
}
}
private func userView(_ u: UserInfo) -> some View {
let user = u.user
return Button(action: {
if user.activeUser {
showSettings = true
withAnimation {
userPickerVisible.toggle()
}
} else {
// This check prevents the call of listUsers after the app is suspended, and the database is closed.
if case .active = scenePhase {
currentUser = m.currentUser?.userId
Task {
do {
try await changeActiveUserAsync_(user.userId, viewPwd: nil)
await MainActor.run { userPickerVisible = false }
} catch {
let users = try await listUsersAsync()
await MainActor.run {
AlertManager.shared.showAlertMsg(
title: "Error switching profile!",
message: "Error: \(responseError(error))"
)
m.users = users
currentUser = m.currentUser?.userId
}
} catch {
logger.error("Error loading users \(responseError(error))")
}
}
}
}, label: {
HStack(spacing: 0) {
ProfileImage(imageStr: user.image, size: 44, color: Color(uiColor: .tertiarySystemFill))
.padding(.trailing, 12)
Text(user.chatViewName)
.fontWeight(user.activeUser ? .medium : .regular)
.foregroundColor(theme.colors.onBackground)
.overlay(DetermineWidth())
Spacer()
if user.activeUser {
Image(systemName: "checkmark")
} else if u.unreadCount > 0 {
unreadCounter(u.unreadCount, color: user.showNtfs ? theme.colors.primary : theme.colors.secondary)
} else if !user.showNtfs {
Image(systemName: "speaker.slash")
}
.onChange(of: userPickerShown) {
if !$0 { resetScroll() }
}
.modifier(ThemedBackground(grouped: true))
.disabled(switchingProfile)
}
private func userView(_ u: UserInfo, size: CGFloat) -> some View {
HStack {
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: u.user.image, size: size, color: Color(uiColor: .tertiarySystemGroupedBackground))
if (u.unreadCount > 0) {
unreadBadge(u).offset(x: 4, y: -4)
}
}
.padding(.trailing)
.padding([.leading, .vertical], 12)
})
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
}
private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 0) {
Text(title)
.overlay(DetermineWidth())
Spacer()
Image(systemName: icon)
.symbolRenderingMode(.monochrome)
.foregroundColor(theme.colors.secondary)
}
.padding(.horizontal)
.padding(.vertical, 22)
.frame(height: menuButtonHeight)
.padding(.trailing, 6)
Text(u.user.displayName).font(.title2).lineLimit(1)
}
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
.padding(rowPadding)
.modifier(ListRow {
switchingProfile = true
Task {
do {
try await changeActiveUserAsync_(u.user.userId, viewPwd: nil)
await MainActor.run {
switchingProfile = false
userPickerShown = false
}
} catch {
await MainActor.run {
switchingProfile = false
showAlert(
NSLocalizedString("Error switching profile!", comment: "alertTitle"),
message: String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "alert message"), responseError(error))
)
}
}
}
})
.clipShape(sectionShape)
}
private func openSheetOnTap(_ icon: String, title: LocalizedStringKey, sheet: UserPickerSheet, showDivider: Bool = true, disabled: Bool = false) -> some View {
ZStack(alignment: .bottom) {
settingsRow(icon, color: theme.colors.secondary) {
Text(title).foregroundColor(.primary).opacity(disabled ? 0.4 : 1)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, rowPadding)
.padding(.vertical, rowVerticalPadding)
.modifier(ListRow { activeSheet = sheet })
.disabled(disabled)
if showDivider {
Divider().padding(.leading, 52)
}
}
}
private func unreadBadge(_ u: UserInfo) -> some View {
let size = dynamicSize(userFont).chatInfoSize
return unreadCountText(u.unreadCount)
.font(userFont <= .xxxLarge ? .caption : .caption2)
.foregroundColor(.white)
.padding(.horizontal, dynamicSize(userFont).unreadPadding)
.frame(minWidth: size, minHeight: size)
.background(u.user.showNtfs ? theme.colors.primary : theme.colors.secondary)
.cornerRadius(dynamicSize(userFont).unreadCorner)
}
}
private func unreadCounter(_ unread: Int, color: Color) -> some View {
unreadCountText(unread)
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 4)
.frame(minWidth: 18, minHeight: 18)
.background(color)
.cornerRadius(10)
struct ListRow: ViewModifier {
@Environment(\.colorScheme) private var colorScheme: ColorScheme
@State private var touchDown = false
let action: () -> Void
func body(content: Content) -> some View {
ZStack {
elevatedSecondarySystemGroupedBackground
Color(.systemGray4).opacity(touchDown ? 1 : 0)
content
TouchOverlay(touchDown: $touchDown, action: action)
}
}
var elevatedSecondarySystemGroupedBackground: Color {
switch colorScheme {
case .dark: Color(0xFF2C2C2E)
default: Color(0xFFFFFFFF)
}
}
struct TouchOverlay: UIViewRepresentable {
@Binding var touchDown: Bool
let action: () -> Void
func makeUIView(context: Context) -> TouchView {
let touchView = TouchView()
let gesture = UILongPressGestureRecognizer(
target: touchView,
action: #selector(touchView.longPress(gesture:))
)
gesture.delegate = touchView
gesture.minimumPressDuration = 0
touchView.addGestureRecognizer(gesture)
return touchView
}
func updateUIView(_ touchView: TouchView, context: Context) {
touchView.representer = self
}
class TouchView: UIView, UIGestureRecognizerDelegate {
var representer: TouchOverlay?
private var startLocation: CGPoint?
private var task: Task<Void, Never>?
@objc
func longPress(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
startLocation = gesture.location(in: nil)
task = Task {
do {
try await Task.sleep(nanoseconds: 200_000000)
await MainActor.run { representer?.touchDown = true }
} catch { }
}
case .ended:
if hitTest(gesture.location(in: self), with: nil) == self {
representer?.action()
}
task?.cancel()
representer?.touchDown = false
case .changed:
if let startLocation {
let location = gesture.location(in: nil)
let dx = location.x - startLocation.x
let dy = location.y - startLocation.y
if sqrt(pow(dx, 2) + pow(dy, 2)) > 10 { gesture.state = .failed }
}
case .cancelled, .failed:
task?.cancel()
representer?.touchDown = false
default: break
}
}
func gestureRecognizer(
_: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith: UIGestureRecognizer
) -> Bool { true }
}
}
}
struct UserPicker_Previews: PreviewProvider {
static var previews: some View {
@State var activeSheet: UserPickerSheet?
let m = ChatModel()
m.users = [UserInfo.sampleData, UserInfo.sampleData]
return UserPicker(
showSettings: Binding.constant(false),
showConnectDesktop: Binding.constant(false),
userPickerVisible: Binding.constant(true)
userPickerShown: .constant(true),
activeSheet: $activeSheet
)
.environmentObject(m)
}
@@ -44,7 +44,7 @@ enum DatabaseAlert: Identifiable {
struct DatabaseView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool
let dismissSettingsSheet: DismissAction
@State private var runChat = false
@State private var alert: DatabaseAlert? = nil
@State private var showFileImporter = false
@@ -270,12 +270,12 @@ struct DatabaseView: View {
case let .archiveImportedWithErrors(errs):
return Alert(
title: Text("Chat database imported"),
message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs)
message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs)
)
case let .archiveExportedWithErrors(archivePath, errs):
return Alert(
title: Text("Chat database exported"),
message: Text("You may save the exported archive.") + Text(verbatim: "\n\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
message: Text("You may save the exported archive.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
dismissButton: .default(Text("Continue")) {
showShareSheet(items: [archivePath])
}
@@ -439,7 +439,7 @@ struct DatabaseView: View {
private func startChat() {
if m.chatDbChanged {
showSettings = false
dismissSettingsSheet()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
resetChatCtrl()
do {
@@ -491,7 +491,7 @@ struct DatabaseView: View {
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
do {
let chats = try apiGetChats()
m.updateChats(with: chats)
m.updateChats(chats)
} catch let error {
logger.error("apiGetChats: cannot update chats \(responseError(error))")
}
@@ -533,7 +533,9 @@ func deleteChatAsync() async throws {
}
struct DatabaseView_Previews: PreviewProvider {
@Environment(\.dismiss) static var mockDismiss
static var previews: some View {
DatabaseView(showSettings: Binding.constant(false), chatItemTTL: .none)
DatabaseView(dismissSettingsSheet: mockDismiss, chatItemTTL: .none)
}
}
@@ -14,50 +14,55 @@ import SimpleXChat
/// Supports [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically)
/// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius
struct ChatItemClipped: ViewModifier {
struct ClipShape: Shape {
let maxCornerRadius: Double
func path(in rect: CGRect) -> Path {
Path(
roundedRect: rect,
cornerRadius: min((rect.height / 2), maxCornerRadius),
style: .circular
)
}
}
@AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var tailEnabled = true
private let chatItem: (content: CIContent, chatDir: CIDirection)?
private let tailVisible: Bool
init() {
clipShape = ClipShape(
maxCornerRadius: 18
)
self.chatItem = nil
self.tailVisible = false
}
init(_ ci: ChatItem, tailVisible: Bool) {
self.chatItem = (ci.content, ci.chatDir)
self.tailVisible = tailVisible
}
init(_ chatItem: ChatItem) {
clipShape = ClipShape(
maxCornerRadius: {
switch chatItem.content {
case
.sndMsgContent,
private func shapeStyle() -> ChatItemShape.Style {
if let ci = chatItem {
switch ci.content {
case
.sndMsgContent,
.rcvMsgContent,
.rcvDecryptionError,
.rcvGroupInvitation,
.sndGroupInvitation,
.sndDeleted,
.rcvDeleted,
.rcvIntegrityError,
.sndModerated,
.rcvModerated,
.rcvBlocked,
.invalidJSON: 18
default: 8
.invalidJSON:
let tail = if let mc = ci.content.msgContent, mc.isImageOrVideo && mc.text.isEmpty {
false
} else {
tailVisible
}
}()
)
return tailEnabled
? .bubble(
padding: ci.chatDir.sent ? .trailing : .leading,
tailVisible: tail
)
: .roundRect(radius: msgRectMaxRadius)
case .rcvGroupInvitation, .sndGroupInvitation:
return .roundRect(radius: msgRectMaxRadius)
default: return .roundRect(radius: 8)
}
} else {
return .roundRect(radius: msgRectMaxRadius)
}
}
private let clipShape: ClipShape
func body(content: Content) -> some View {
let clipShape = ChatItemShape(
roundness: roundness,
style: shapeStyle()
)
content
.contentShape(.dragPreview, clipShape)
.contentShape(.contextMenuPreview, clipShape)
@@ -65,4 +70,106 @@ struct ChatItemClipped: ViewModifier {
}
}
struct ChatTailPadding: ViewModifier {
func body(content: Content) -> some View {
content.padding(.horizontal, -msgTailWidth)
}
}
private let msgRectMaxRadius: Double = 18
private let msgBubbleMaxRadius: Double = msgRectMaxRadius * 1.2
private let msgTailWidth: Double = 9
private let msgTailMinHeight: Double = msgTailWidth * 1.254 // ~56deg
private let msgTailMaxHeight: Double = msgTailWidth * 1.732 // 60deg
struct ChatItemShape: Shape {
fileprivate enum Style {
case bubble(padding: HorizontalEdge, tailVisible: Bool)
case roundRect(radius: Double)
}
fileprivate let roundness: Double
fileprivate let style: Style
func path(in rect: CGRect) -> Path {
switch style {
case let .bubble(padding, tailVisible):
let w = rect.width
let h = rect.height
let rxMax = min(msgBubbleMaxRadius, w / 2)
let ryMax = min(msgBubbleMaxRadius, h / 2)
let rx = roundness * rxMax
let ry = roundness * ryMax
let tailHeight = min(msgTailMinHeight + roundness * (msgTailMaxHeight - msgTailMinHeight), h / 2)
var path = Path()
// top side
path.move(to: CGPoint(x: rx, y: 0))
path.addLine(to: CGPoint(x: w - rx, y: 0))
if roundness > 0 {
// top-right corner
path.addQuadCurve(to: CGPoint(x: w, y: ry), control: CGPoint(x: w, y: 0))
}
if rect.height > 2 * ry {
// right side
path.addLine(to: CGPoint(x: w, y: h - ry))
}
if roundness > 0 {
// bottom-right corner
path.addQuadCurve(to: CGPoint(x: w - rx, y: h), control: CGPoint(x: w, y: h))
}
// bottom side
if tailVisible {
path.addLine(to: CGPoint(x: -msgTailWidth, y: h))
if roundness > 0 {
// bottom-left tail
// distance of control point from touch point, calculated via ratios
let d = tailHeight - msgTailWidth * msgTailWidth / tailHeight
// tail control point
let tc = CGPoint(x: 0, y: h - tailHeight + d * sqrt(roundness))
// bottom-left tail curve
path.addQuadCurve(to: CGPoint(x: 0, y: h - tailHeight), control: tc)
} else {
path.addLine(to: CGPoint(x: 0, y: h - tailHeight))
}
if rect.height > ry + tailHeight {
// left side
path.addLine(to: CGPoint(x: 0, y: ry))
}
} else {
path.addLine(to: CGPoint(x: rx, y: h))
path.addQuadCurve(to: CGPoint(x: 0, y: h - ry), control: CGPoint(x: 0 , y: h))
if rect.height > 2 * ry {
// left side
path.addLine(to: CGPoint(x: 0, y: ry))
}
}
if roundness > 0 {
// top-left corner
path.addQuadCurve(to: CGPoint(x: rx, y: 0), control: CGPoint(x: 0, y: 0))
}
path.closeSubpath()
return switch padding {
case .leading: path
case .trailing: path
.scale(x: -1, y: 1, anchor: .center)
.path(in: rect)
}
case let .roundRect(radius):
return Path(roundedRect: rect, cornerRadius: radius * roundness)
}
}
var offset: Double? {
switch style {
case let .bubble(padding, isTailVisible):
if isTailVisible {
switch padding {
case .leading: -msgTailWidth
case .trailing: msgTailWidth
}
} else { 0 }
case .roundRect: 0
}
}
}
@@ -100,7 +100,7 @@ extension PresetWallpaper {
scale
} else if let type = ChatModel.shared.currentUser?.uiThemes?.preferredMode(base.mode == DefaultThemeMode.dark)?.wallpaper?.toAppWallpaper().type, type.sameType(WallpaperType.preset(filename, nil)) {
type.scale
} else if let scale = themeOverridesDefault.get().first(where: { $0.wallpaper != nil && $0.wallpaper!.preset == filename })?.wallpaper?.scale {
} else if let scale = themeOverridesDefault.get().first(where: { $0.wallpaper != nil && $0.wallpaper!.preset == filename && $0.base == base })?.wallpaper?.scale {
scale
} else {
Float(1.0)
@@ -0,0 +1,21 @@
//
// Test.swift
// SimpleX (iOS)
//
// Created by Levitating Pineapple on 31/08/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
extension View {
@ViewBuilder
func invertedForegroundStyle(enabled: Bool = true) -> some View {
if enabled {
foregroundStyle(Material.ultraThin)
.environment(\.colorScheme, .dark)
.grayscale(1)
.contrast(-20)
} else { self }
}
}
@@ -20,7 +20,7 @@ struct ProfileImage: View {
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
var body: some View {
if let uiImage = UIImage(base64Encoded: imageStr) {
if let uiImage = imageFromBase64(imageStr) {
clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius, blurred: blurred)
} else {
let c = color.asAnotherColorFromSecondaryVariant(theme)
+54 -6
View File
@@ -8,15 +8,63 @@
import SwiftUI
func showShareSheet(items: [Any], completed: (() -> Void)? = nil) {
func getTopViewController() -> UIViewController? {
let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene
if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first,
let presentedViewController = keyWindow.rootViewController?.presentedViewController ?? keyWindow.rootViewController {
let rootViewController = keyWindow.rootViewController {
// Find the top-most presented view controller
var topController = rootViewController
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
return nil
}
func showShareSheet(items: [Any], completed: (() -> Void)? = nil) {
if let topController = getTopViewController() {
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
if let completed = completed {
let handler: UIActivityViewController.CompletionWithItemsHandler = { _,_,_,_ in completed() }
activityViewController.completionWithItemsHandler = handler
}
presentedViewController.present(activityViewController, animated: true)
activityViewController.completionWithItemsHandler = { _, _, _, _ in
completed()
}
}
topController.present(activityViewController, animated: true)
}
}
func showAlert(
title: String,
message: String? = nil,
buttonTitle: String,
buttonAction: @escaping () -> Void,
cancelButton: Bool
) -> Void {
if let topController = getTopViewController() {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: buttonTitle, style: .default) { _ in
buttonAction()
})
if cancelButton {
alert.addAction(cancelAlertAction)
}
topController.present(alert, animated: true)
}
}
func showAlert(
_ title: String,
message: String? = nil,
actions: () -> [UIAlertAction] = { [okAlertAction] }
) {
if let topController = getTopViewController() {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
for action in actions() { alert.addAction(action) }
topController.present(alert, animated: true)
}
}
let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default)
let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel)
@@ -0,0 +1,188 @@
//
// SwiftUISheet.swift
// SimpleX (iOS)
//
// Created by user on 23/09/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
private let sheetAnimationDuration: Double = 0.35
// Refrence: https://easings.net/
private let easeOutCubic = UICubicTimingParameters(
controlPoint1: CGPoint(x: 0.215, y: 0.61),
controlPoint2: CGPoint(x: 0.355, y: 1)
)
struct Sheet<SheetContent: View>: ViewModifier {
@Binding var isPresented: Bool
@ViewBuilder let sheetContent: () -> SheetContent
func body(content: Content) -> some View {
ZStack {
content
SheetRepresentable(isPresented: $isPresented, content: sheetContent())
.allowsHitTesting(isPresented)
.ignoresSafeArea()
}
}
}
struct SheetRepresentable<Content: View>: UIViewControllerRepresentable {
@Binding var isPresented: Bool
let content: Content
func makeUIViewController(context: Context) -> Controller<Content> {
Controller(content: content, representer: self)
}
func updateUIViewController(_ sheetController: Controller<Content>, context: Context) {
sheetController.animate(isPresented: isPresented)
}
class Controller<C: View>: UIViewController {
let hostingController: UIHostingController<C>
private let animator = UIViewPropertyAnimator(
duration: sheetAnimationDuration,
timingParameters: easeOutCubic
)
private let representer: SheetRepresentable<C>
private var retainedFraction: CGFloat = 0
private var sheetHeight: Double { hostingController.view.frame.height }
private var task: Task<Void, Never>?
init(content: C, representer: SheetRepresentable<C>) {
self.representer = representer
self.hostingController = UIHostingController(rootView: content)
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) missing") }
deinit {
animator.stopAnimation(true)
animator.finishAnimation(at: .current)
}
func animate(isPresented: Bool) {
let alreadyAnimating = animator.isRunning && isPresented != animator.isReversed
let sheetFullyDismissed = animator.fractionComplete == (animator.isReversed ? 1 : 0)
let sheetFullyPresented = animator.fractionComplete == (animator.isReversed ? 0 : 1)
if !isPresented && sheetFullyDismissed ||
isPresented && sheetFullyPresented ||
alreadyAnimating {
return
}
animator.pauseAnimation()
animator.isReversed = !isPresented
animator.continueAnimation(
withTimingParameters: isPresented
? easeOutCubic
: UICubicTimingParameters(animationCurve: .easeIn),
durationFactor: 1 - animator.fractionComplete
)
handleVisibility()
}
func handleVisibility() {
if animator.isReversed {
task = Task {
do {
let sleepDuration = UInt64(sheetAnimationDuration * Double(NSEC_PER_SEC))
try await Task.sleep(nanoseconds: sleepDuration)
view.isHidden = true
} catch { }
}
} else {
task?.cancel()
task = nil
view.isHidden = false
}
}
override func viewDidLoad() {
view.isHidden = true
view.backgroundColor = .clear
view.addGestureRecognizer(
UITapGestureRecognizer(target: self, action: #selector(tap(gesture:)))
)
addChild(hostingController)
hostingController.didMove(toParent: self)
if let sheet = hostingController.view {
sheet.isHidden = true
sheet.clipsToBounds = true
sheet.layer.cornerRadius = 10
sheet.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
sheet.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(pan(gesture:))))
sheet.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(sheet)
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
}
override func viewDidAppear(_ animated: Bool) {
// Ensures animations are only setup once
// on some iOS version `viewDidAppear` can get called on each state change.
if hostingController.view.isHidden {
hostingController.view.transform = CGAffineTransform(translationX: 0, y: self.sheetHeight)
hostingController.view.isHidden = false
animator.pausesOnCompletion = true
animator.addAnimations {
self.hostingController.view.transform = .identity
self.view.backgroundColor = UIColor {
switch $0.userInterfaceStyle {
case .dark: .black.withAlphaComponent(0.290)
default: .black.withAlphaComponent(0.121)
}
}
}
animator.startAnimation()
animator.pauseAnimation()
}
}
@objc
func pan(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
animator.isReversed = false
animator.pauseAnimation()
retainedFraction = animator.fractionComplete
case .changed:
animator.fractionComplete = retainedFraction - gesture.translation(in: view).y / sheetHeight
case .ended, .cancelled:
let velocity = gesture.velocity(in: view).y
animator.isReversed = (velocity - (animator.fractionComplete - 0.5) * 100).sign == .plus
let defaultVelocity = sheetHeight / sheetAnimationDuration
let fractionRemaining = 1 - animator.fractionComplete
let durationFactor = min(max(fractionRemaining / (abs(velocity) / defaultVelocity), 0.5), 1)
animator.continueAnimation(withTimingParameters: nil, durationFactor: durationFactor * fractionRemaining)
handleVisibility()
DispatchQueue.main.asyncAfter(deadline: .now() + sheetAnimationDuration) {
self.representer.isPresented = !self.animator.isReversed
}
default: break
}
}
@objc
func tap(gesture: UITapGestureRecognizer) {
switch gesture.state {
case .ended:
if gesture.location(in: view).y < view.frame.height - sheetHeight {
representer.isPresented = false
}
default: break
}
}
}
}
@@ -0,0 +1,61 @@
//
// StickyScrollView.swift
// SimpleX (iOS)
//
// Created by user on 20/09/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct StickyScrollView<Content: View>: UIViewRepresentable {
@Binding var resetScroll: ResetScrollAction
@ViewBuilder let content: () -> Content
func makeUIView(context: Context) -> UIScrollView {
let hc = context.coordinator.hostingController
hc.view.backgroundColor = .clear
let sv = UIScrollView()
sv.showsHorizontalScrollIndicator = false
sv.addSubview(hc.view)
sv.delegate = context.coordinator
DispatchQueue.main.async {
resetScroll = ResetScrollAction { sv.setContentOffset(.zero, animated: false) }
}
return sv
}
func updateUIView(_ scrollView: UIScrollView, context: Context) {
let hc = context.coordinator.hostingController
hc.rootView = content()
hc.view.frame.size = hc.view.intrinsicContentSize
scrollView.contentSize = hc.view.intrinsicContentSize
}
func makeCoordinator() -> Coordinator {
Coordinator(content: content())
}
class Coordinator: NSObject, UIScrollViewDelegate {
let hostingController: UIHostingController<Content>
init(content: Content) {
self.hostingController = UIHostingController(rootView: content)
}
func scrollViewWillEndDragging(
_ scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>
) {
if targetContentOffset.pointee.x < 32 {
targetContentOffset.pointee.x = 0
}
}
}
}
struct ResetScrollAction {
var action = { }
func callAsFunction() { action() }
}
@@ -65,8 +65,7 @@ struct LocalAuthView: View {
// Clear sensitive data on screen just in case app fails to hide its views while new database is created
m.chatId = nil
ItemsModel.shared.reversedChatItems = []
m.chats = []
m.popChatCollector.clear()
m.updateChats([])
m.users = []
_ = kcAppPassword.set(password)
_ = kcSelfDestructPassword.remove()
@@ -24,6 +24,7 @@ private enum MigrationFromState: Equatable {
}
private enum MigrateFromDeviceViewAlert: Identifiable {
case finishMigration(_ fileId: Int64, _ ctrl: chat_ctrl)
case deleteChat(_ title: LocalizedStringKey = "Delete chat profile?", _ text: LocalizedStringKey = "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.")
case startChat(_ title: LocalizedStringKey = "Start chat?", _ text: LocalizedStringKey = "Warning: starting chat on multiple devices is not supported and will cause message delivery failures")
@@ -38,6 +39,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
var id: String {
switch self {
case .finishMigration: return "finishMigration"
case let .deleteChat(title, text): return "\(title) \(text)"
case let .startChat(title, text): return "\(title) \(text)"
@@ -56,8 +58,6 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
struct MigrateFromDevice: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@Binding var showSettings: Bool
@Binding var showProgressOnSettings: Bool
@State private var migrationState: MigrationFromState = .chatStopInProgress
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@@ -106,9 +106,6 @@ struct MigrateFromDevice: View {
finishedView(chatDeletion)
}
}
.modifier(BackButton(label: "Back", disabled: $backDisabled) {
dismiss()
})
.onChange(of: migrationState) { state in
backDisabled = switch migrationState {
case .chatStopInProgress, .archiving, .linkShown, .finished: true
@@ -138,6 +135,15 @@ struct MigrateFromDevice: View {
}
.alert(item: $alert) { alert in
switch alert {
case let .finishMigration(fileId, ctrl):
return Alert(
title: Text("Remove archive?"),
message: Text("The uploaded database archive will be permanently removed from the servers."),
primaryButton: .destructive(Text("Continue")) {
finishMigration(fileId, ctrl)
},
secondaryButton: .cancel()
)
case let .startChat(title, text):
return Alert(
title: Text(title),
@@ -171,7 +177,7 @@ struct MigrateFromDevice: View {
case let .archiveExportedWithErrors(archivePath, errs):
return Alert(
title: Text("Chat database exported"),
message: Text("You may migrate the exported database.") + Text(verbatim: "\n\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
message: Text("You may migrate the exported database.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
dismissButton: .default(Text("Continue")) {
Task { await uploadArchive(path: archivePath) }
}
@@ -318,7 +324,7 @@ struct MigrateFromDevice: View {
Text("Cancel migration").foregroundColor(.red)
}
}
Button(action: { finishMigration(fileId, ctrl) }) {
Button(action: { alert = .finishMigration(fileId, ctrl) }) {
settingsRow("checkmark", color: theme.colors.secondary) {
Text("Finalize migration").foregroundColor(theme.colors.primary)
}
@@ -523,9 +529,15 @@ struct MigrateFromDevice: View {
}
case let .sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs):
let cfg = getNetCfg()
let proxy: NetworkProxy? = if cfg.socksProxy == nil {
nil
} else {
networkProxyDefault.get()
}
let data = MigrationFileLinkData.init(
networkConfig: MigrationFileLinkData.NetworkConfig(
socksProxy: cfg.socksProxy,
networkProxy: proxy,
hostMode: cfg.hostMode,
requiredHostMode: cfg.requiredHostMode
)
@@ -590,7 +602,7 @@ struct MigrateFromDevice: View {
} catch let error {
fatalError("Error starting chat \(responseError(error))")
}
showSettings = false
dismissAllSheets(animated: true)
}
} catch let error {
alert = .error(title: "Error deleting database", error: responseError(error))
@@ -613,9 +625,7 @@ struct MigrateFromDevice: View {
}
// Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered
if dismiss || m.chatDbStatus != .ok {
await MainActor.run {
showSettings = false
}
dismissAllSheets(animated: true)
}
}
@@ -767,6 +777,6 @@ private class MigrationChatReceiver {
struct MigrateFromDevice_Previews: PreviewProvider {
static var previews: some View {
MigrateFromDevice(showSettings: Binding.constant(true), showProgressOnSettings: Binding.constant(false))
MigrateFromDevice(showProgressOnSettings: Binding.constant(false))
}
}
@@ -93,7 +93,6 @@ struct MigrateToDevice: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@Binding var migrationState: MigrationToState?
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@State private var alert: MigrateToDeviceViewAlert?
@@ -102,6 +101,7 @@ struct MigrateToDevice: View {
// Prevent from hiding the view until migration is finished or app deleted
@State private var backDisabled: Bool = false
@State private var showQRCodeScanner: Bool = true
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
var body: some View {
VStack {
@@ -197,10 +197,8 @@ struct MigrateToDevice: View {
}
}
}
if developerTools {
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
pasteLinkView()
}
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
pasteLinkView()
}
}
}
@@ -218,7 +216,7 @@ struct MigrateToDevice: View {
} label: {
Text("Tap to paste link")
}
.disabled(!ChatModel.shared.pasteboardHasStrings)
.disabled(!pasteboardHasStrings)
.frame(maxWidth: .infinity, alignment: .center)
}
@@ -487,6 +485,9 @@ struct MigrateToDevice: View {
do {
if !hasChatCtrl() {
chatInitControllerRemovingDatabases()
} else if ChatModel.shared.chatRunning == true {
// cannot delete storage if chat is running
try await apiStopChat()
}
try await apiDeleteStorage()
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
@@ -556,11 +557,22 @@ struct MigrateToDevice: View {
do {
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
MigrationToDeviceState.save(nil)
appSettings.importIntoApp()
try SimpleX.startChat(refreshInvitations: true)
AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.")
try ObjC.catchException {
appSettings.importIntoApp()
}
do {
try SimpleX.startChat(refreshInvitations: true)
AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.")
} catch let error {
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
}
} catch let error {
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
logger.error("Error importing settings: \(error.localizedDescription)")
AlertManager.shared.showAlert(
Alert(
title: Text("Error migrating settings"),
message: Text ("Some app settings were not migrated.") + Text("\n") + Text(responseError(error)))
)
}
hideView()
}
@@ -28,7 +28,9 @@ struct AddContactLearnMore: View {
Text("If you can't meet in person, show QR code in a video call, or share the link.")
Text("Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).")
}
.frame(maxWidth: .infinity, alignment: .leading)
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.modifier(ThemedBackground(grouped: true))
}
@@ -137,10 +137,13 @@ struct AddGroupView: View {
createInvalidNameAlert(mkValidName(profile.displayName), $profile.displayName)
}
.onChange(of: chosenImage) { image in
if let image = image {
profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
} else {
profile.image = nil
Task {
let resized: String? = if let image {
await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
} else {
nil
}
await MainActor.run { profile.image = resized }
}
}
.modifier(ThemedBackground(grouped: true))
@@ -14,9 +14,11 @@ enum ContactType: Int {
}
struct NewChatMenuButton: View {
// do not use chatModel here because it prevents showing AddGroupMembersView after group creation and QR code after link creation on iOS 16
// @EnvironmentObject var chatModel: ChatModel
@State private var showNewChatSheet = false
@State private var alert: SomeAlert? = nil
@State private var globalAlert: SomeAlert? = nil
@State private var pendingConnection: PendingContactConnection? = nil
var body: some View {
Button {
@@ -28,22 +30,14 @@ struct NewChatMenuButton: View {
.frame(width: 24, height: 24)
}
.appSheet(isPresented: $showNewChatSheet) {
NewChatSheet(alert: $alert)
NewChatSheet(pendingConnection: $pendingConnection)
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
.alert(item: $alert) { a in
return a.alert
.onDisappear {
alert = cleanupPendingConnection(contactConnection: pendingConnection)
pendingConnection = nil
}
}
// This is a workaround to show "Keep unused invitation" alert in both following cases:
// - on going back from NewChatView to NewChatSheet,
// - on dismissing NewChatMenuButton sheet while on NewChatView (skipping NewChatSheet)
.onChange(of: alert?.id) { a in
if !showNewChatSheet && alert != nil {
globalAlert = alert
alert = nil
}
}
.alert(item: $globalAlert) { a in
.alert(item: $alert) { a in
return a.alert
}
}
@@ -60,7 +54,8 @@ struct NewChatSheet: View {
@State private var searchText = ""
@State private var searchShowingSimplexLink = false
@State private var searchChatFilteredBySimplexLink: String? = nil
@Binding var alert: SomeAlert?
@State private var alert: SomeAlert?
@Binding var pendingConnection: PendingContactConnection?
// Sheet height management
@State private var isAddContactActive = false
@@ -78,6 +73,9 @@ struct NewChatSheet: View {
.navigationBarTitleDisplayMode(.large)
.navigationBarHidden(searchMode)
.modifier(ThemedBackground(grouped: true))
.alert(item: $alert) { a in
return a.alert
}
}
if #available(iOS 16.0, *), oneHandUI {
let sheetHeight: CGFloat = showArchive ? 575 : 500
@@ -112,7 +110,7 @@ struct NewChatSheet: View {
if (searchText.isEmpty) {
Section {
NavigationLink(isActive: $isAddContactActive) {
NewChatView(selection: .invite, parentAlert: $alert)
NewChatView(selection: .invite, parentAlert: $alert, contactConnection: $pendingConnection)
.navigationTitle("New chat")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
@@ -122,7 +120,7 @@ struct NewChatSheet: View {
}
}
NavigationLink(isActive: $isScanPasteLinkActive) {
NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert)
NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert, contactConnection: $pendingConnection)
.navigationTitle("New chat")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
@@ -199,7 +197,7 @@ func chatContactType(chat: Chat) -> ContactType {
case .contactRequest:
return .request
case let .direct(contact):
if contact.activeConn == nil && contact.profile.contactLink != nil {
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
return .card
} else if contact.chatDeleted {
return .chatDeleted
+348 -46
View File
@@ -45,18 +45,47 @@ enum NewChatOption: Identifiable {
var id: Self { self }
}
func cleanupPendingConnection(contactConnection: PendingContactConnection?) -> SomeAlert? {
var alert: SomeAlert? = nil
if !(ChatModel.shared.showingInvitation?.connChatUsed ?? true),
let conn = contactConnection {
alert = SomeAlert(
alert: Alert(
title: Text("Keep unused invitation?"),
message: Text("You can view invitation link again in connection details."),
primaryButton: .default(Text("Keep")) {},
secondaryButton: .destructive(Text("Delete")) {
Task {
await deleteChat(Chat(
chatInfo: .contactConnection(contactConnection: conn),
chatItems: []
))
}
}
),
id: "keepUnusedInvitation"
)
}
ChatModel.shared.showingInvitation = nil
return alert
}
struct NewChatView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@State var selection: NewChatOption
@State var showQRCodeScanner = false
@State private var invitationUsed: Bool = false
@State private var contactConnection: PendingContactConnection? = nil
@State private var connReqInvitation: String = ""
@State private var creatingConnReq = false
@State var choosingProfile = false
@State private var pastedLink: String = ""
@State private var alert: NewChatViewAlert?
@Binding var parentAlert: SomeAlert?
@Binding var contactConnection: PendingContactConnection?
var body: some View {
VStack(alignment: .leading) {
@@ -68,6 +97,11 @@ struct NewChatView: View {
}
.pickerStyle(.segmented)
.padding()
.onChange(of: $selection.wrappedValue) { opt in
if opt == NewChatOption.connect {
showQRCodeScanner = true
}
}
VStack {
// it seems there's a bug in iOS 15 if several views in switch (or if-else) statement have different transitions
@@ -122,26 +156,10 @@ struct NewChatView: View {
}
}
.onDisappear {
if !(m.showingInvitation?.connChatUsed ?? true),
let conn = contactConnection {
parentAlert = SomeAlert(
alert: Alert(
title: Text("Keep unused invitation?"),
message: Text("You can view invitation link again in connection details."),
primaryButton: .default(Text("Keep")) {},
secondaryButton: .destructive(Text("Delete")) {
Task {
await deleteChat(Chat(
chatInfo: .contactConnection(contactConnection: conn),
chatItems: []
))
}
}
),
id: "keepUnusedInvitation"
)
if !choosingProfile {
parentAlert = cleanupPendingConnection(contactConnection: contactConnection)
contactConnection = nil
}
m.showingInvitation = nil
}
.alert(item: $alert) { a in
switch(a) {
@@ -159,7 +177,8 @@ struct NewChatView: View {
InviteView(
invitationUsed: $invitationUsed,
contactConnection: $contactConnection,
connReqInvitation: connReqInvitation
connReqInvitation: $connReqInvitation,
choosingProfile: $choosingProfile
)
} else if creatingConnReq {
creatingLinkProgressView()
@@ -210,12 +229,22 @@ struct NewChatView: View {
}
}
private func incognitoProfileImage() -> some View {
Image(systemName: "theatermasks.fill")
.resizable()
.scaledToFit()
.frame(width: 30)
.foregroundColor(.indigo)
}
private struct InviteView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var invitationUsed: Bool
@Binding var contactConnection: PendingContactConnection?
var connReqInvitation: String
@Binding var connReqInvitation: String
@Binding var choosingProfile: Bool
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
var body: some View {
@@ -226,28 +255,40 @@ private struct InviteView: View {
.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10))
qrCodeView()
Section {
IncognitoToggle(incognitoEnabled: $incognitoDefault)
} footer: {
sharedProfileInfo(incognitoDefault)
.foregroundColor(theme.colors.secondary)
if let selectedProfile = chatModel.currentUser {
Section {
NavigationLink {
ActiveProfilePicker(
contactConnection: $contactConnection,
connReqInvitation: $connReqInvitation,
incognitoEnabled: $incognitoDefault,
choosingProfile: $choosingProfile,
selectedProfile: selectedProfile
)
} label: {
HStack {
if incognitoDefault {
incognitoProfileImage()
Text("Incognito")
} else {
ProfileImage(imageStr: chatModel.currentUser?.image, size: 30)
Text(chatModel.currentUser?.chatViewName ?? "")
}
}
}
} header: {
Text("Share profile").foregroundColor(theme.colors.secondary)
} footer: {
if incognitoDefault {
Text("A new random profile will be shared.")
}
}
}
}
.onChange(of: incognitoDefault) { incognito in
Task {
do {
if let contactConn = contactConnection,
let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
await MainActor.run {
contactConnection = conn
chatModel.updateContactConnection(conn)
}
}
} catch {
logger.error("apiSetConnectionIncognito error: \(responseError(error))")
}
}
setInvitationUsed()
}
.onChange(of: chatModel.currentUser) { u in
setInvitationUsed()
}
}
@@ -270,6 +311,7 @@ private struct InviteView: View {
private func qrCodeView() -> some View {
Section(header: Text("Or show this code").foregroundColor(theme.colors.secondary)) {
SimpleXLinkQRCode(uri: connReqInvitation, onShare: setInvitationUsed)
.id("simplex-qrcode-view-for-\(connReqInvitation)")
.padding()
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
@@ -289,6 +331,257 @@ private struct InviteView: View {
}
}
private enum ProfileSwitchStatus {
case switchingUser
case switchingIncognito
case idle
}
private struct ActiveProfilePicker: View {
@Environment(\.dismiss) var dismiss
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var contactConnection: PendingContactConnection?
@Binding var connReqInvitation: String
@Binding var incognitoEnabled: Bool
@Binding var choosingProfile: Bool
@State private var alert: SomeAlert?
@State private var profileSwitchStatus: ProfileSwitchStatus = .idle
@State private var switchingProfileByTimeout = false
@State private var lastSwitchingProfileByTimeoutCall: Double?
@State private var profiles: [User] = []
@State private var searchTextOrPassword = ""
@State private var showIncognitoSheet = false
@State private var incognitoFirst: Bool = false
@State var selectedProfile: User
var trimmedSearchTextOrPassword: String { searchTextOrPassword.trimmingCharacters(in: .whitespaces)}
var body: some View {
viewBody()
.navigationTitle("Select chat profile")
.searchable(text: $searchTextOrPassword, placement: .navigationBarDrawer(displayMode: .always))
.autocorrectionDisabled(true)
.navigationBarTitleDisplayMode(.large)
.onAppear {
profiles = chatModel.users
.map { $0.user }
.sorted { u, _ in u.activeUser }
}
.onChange(of: incognitoEnabled) { incognito in
if profileSwitchStatus != .switchingIncognito {
return
}
Task {
do {
if let contactConn = contactConnection,
let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
await MainActor.run {
contactConnection = conn
chatModel.updateContactConnection(conn)
profileSwitchStatus = .idle
dismiss()
}
}
} catch {
profileSwitchStatus = .idle
incognitoEnabled = !incognito
logger.error("apiSetConnectionIncognito error: \(responseError(error))")
let err = getErrorAlert(error, "Error changing to incognito!")
alert = SomeAlert(
alert: Alert(
title: Text(err.title),
message: Text(err.message ?? "Error: \(responseError(error))")
),
id: "setConnectionIncognitoError"
)
}
}
}
.onChange(of: profileSwitchStatus) { sp in
if sp != .idle {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
switchingProfileByTimeout = profileSwitchStatus != .idle
}
} else {
switchingProfileByTimeout = false
}
}
.onChange(of: selectedProfile) { profile in
if (profileSwitchStatus != .switchingUser) {
return
}
Task {
do {
if let contactConn = contactConnection,
let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) {
await MainActor.run {
contactConnection = conn
connReqInvitation = conn.connReqInv ?? ""
incognitoEnabled = false
chatModel.updateContactConnection(conn)
}
do {
try await changeActiveUserAsync_(profile.userId, viewPwd: profile.hidden ? trimmedSearchTextOrPassword : nil )
await MainActor.run {
profileSwitchStatus = .idle
dismiss()
}
} catch {
await MainActor.run {
profileSwitchStatus = .idle
alert = SomeAlert(
alert: Alert(
title: Text("Error switching profile"),
message: Text("Your connection was moved to \(profile.chatViewName) but an unexpected error occurred while redirecting you to the profile.")
),
id: "switchingProfileError"
)
}
}
}
} catch {
await MainActor.run {
profileSwitchStatus = .idle
if let currentUser = chatModel.currentUser {
selectedProfile = currentUser
}
let err = getErrorAlert(error, "Error changing connection profile")
alert = SomeAlert(
alert: Alert(
title: Text(err.title),
message: Text(err.message ?? "Error: \(responseError(error))")
),
id: "changeConnectionUserError"
)
}
}
}
}
.alert(item: $alert) { a in
a.alert
}
.onAppear {
incognitoFirst = incognitoEnabled
choosingProfile = true
}
.onDisappear {
choosingProfile = false
}
.sheet(isPresented: $showIncognitoSheet) {
IncognitoHelp()
}
}
@ViewBuilder private func viewBody() -> some View {
profilePicker()
.allowsHitTesting(!switchingProfileByTimeout)
.modifier(ThemedBackground(grouped: true))
.overlay {
if switchingProfileByTimeout {
ProgressView()
.scaleEffect(2)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
private func filteredProfiles() -> [User] {
let s = trimmedSearchTextOrPassword
let lower = s.localizedLowercase
return profiles.filter { u in
if (u.activeUser || !u.hidden) && (s == "" || u.chatViewName.localizedLowercase.contains(lower)) {
return true
}
return correctPassword(u, s)
}
}
@ViewBuilder private func profilerPickerUserOption(_ user: User) -> some View {
Button {
if selectedProfile == user && incognitoEnabled {
incognitoEnabled = false
profileSwitchStatus = .switchingIncognito
} else if selectedProfile != user {
selectedProfile = user
profileSwitchStatus = .switchingUser
}
} label: {
HStack {
ProfileImage(imageStr: user.image, size: 30)
.padding(.trailing, 2)
Text(user.chatViewName)
.foregroundColor(theme.colors.onBackground)
.lineLimit(1)
Spacer()
if selectedProfile == user, !incognitoEnabled {
Image(systemName: "checkmark")
.resizable().scaledToFit().frame(width: 16)
.foregroundColor(theme.colors.primary)
}
}
}
}
@ViewBuilder private func profilePicker() -> some View {
let incognitoOption = Button {
if !incognitoEnabled {
incognitoEnabled = true
profileSwitchStatus = .switchingIncognito
}
} label : {
HStack {
incognitoProfileImage()
Text("Incognito")
.foregroundColor(theme.colors.onBackground)
Image(systemName: "info.circle")
.foregroundColor(theme.colors.primary)
.font(.system(size: 14))
.onTapGesture {
showIncognitoSheet = true
}
Spacer()
if incognitoEnabled {
Image(systemName: "checkmark")
.resizable().scaledToFit().frame(width: 16)
.foregroundColor(theme.colors.primary)
}
}
}
List {
let filteredProfiles = filteredProfiles()
let activeProfile = filteredProfiles.first { u in u.activeUser }
if let selectedProfile = activeProfile {
let otherProfiles = filteredProfiles.filter { u in u.userId != activeProfile?.userId }
if incognitoFirst {
incognitoOption
profilerPickerUserOption(selectedProfile)
} else {
profilerPickerUserOption(selectedProfile)
incognitoOption
}
ForEach(otherProfiles) { p in
profilerPickerUserOption(p)
}
} else {
incognitoOption
ForEach(filteredProfiles) { p in
profilerPickerUserOption(p)
}
}
}
.opacity(switchingProfileByTimeout ? 0.4 : 1)
}
}
private struct ConnectView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var theme: AppTheme
@@ -296,6 +589,7 @@ private struct ConnectView: View {
@Binding var pastedLink: String
@Binding var alert: NewChatViewAlert?
@State private var sheet: PlanAndConnectActionSheet?
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
var body: some View {
List {
@@ -332,7 +626,7 @@ private struct ConnectView: View {
} label: {
Text("Tap to paste link")
}
.disabled(!ChatModel.shared.pasteboardHasStrings)
.disabled(!pasteboardHasStrings)
.frame(maxWidth: .infinity, alignment: .center)
} else {
linkTextView(pastedLink)
@@ -403,6 +697,7 @@ struct ScannerInView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.foregroundColor(Color.clear)
switch cameraAuthorizationStatus {
case .authorized, nil: EmptyView()
case .restricted: Text("Camera not available")
case .denied: Label("Enable camera access", systemImage: "camera")
default: Label("Tap to scan", systemImage: "qrcode")
@@ -422,21 +717,26 @@ struct ScannerInView: View {
.disabled(cameraAuthorizationStatus == .restricted)
}
}
.onAppear {
.task {
let status = AVCaptureDevice.authorizationStatus(for: .video)
cameraAuthorizationStatus = status
if showQRCodeScanner {
switch status {
case .notDetermined: askCameraAuthorization()
case .notDetermined: await askCameraAuthorizationAsync()
case .restricted: showQRCodeScanner = false
case .denied: showQRCodeScanner = false
case .authorized: ()
@unknown default: askCameraAuthorization()
@unknown default: await askCameraAuthorizationAsync()
}
}
}
}
func askCameraAuthorizationAsync() async {
await AVCaptureDevice.requestAccess(for: .video)
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
}
func askCameraAuthorization(_ cb: (() -> Void)? = nil) {
AVCaptureDevice.requestAccess(for: .video) { allowed in
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
@@ -975,10 +1275,12 @@ func connReqSentAlert(_ type: ConnReqType) -> Alert {
struct NewChatView_Previews: PreviewProvider {
static var previews: some View {
@State var parentAlert: SomeAlert?
@State var contactConnection: PendingContactConnection? = nil
NewChatView(
selection: .invite,
parentAlert: $parentAlert
parentAlert: $parentAlert,
contactConnection: $contactConnection
)
}
}
+23 -23
View File
@@ -49,34 +49,34 @@ struct QRCode: View {
ZStack {
if let image = image {
qrCodeImage(image)
}
GeometryReader { geo in
ZStack {
if withLogo {
let w = geo.size.width
Image("icon-light")
.resizable()
.scaledToFit()
.frame(width: w * 0.16, height: w * 0.16)
.frame(width: w * 0.165, height: w * 0.165)
.background(.white)
.clipShape(Circle())
GeometryReader { geo in
ZStack {
if withLogo {
let w = geo.size.width
Image("icon-light")
.resizable()
.scaledToFit()
.frame(width: w * 0.16, height: w * 0.16)
.frame(width: w * 0.165, height: w * 0.165)
.background(.white)
.clipShape(Circle())
}
}
}
.onAppear {
makeScreenshotFunc = {
let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale)
showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])
onShare?()
.onAppear {
makeScreenshotFunc = {
let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale)
showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)])
onShare?()
}
}
.frame(width: geo.size.width, height: geo.size.height)
}
.frame(width: geo.size.width, height: geo.size.height)
} else {
Color.clear.aspectRatio(1, contentMode: .fit)
}
}
.onTapGesture(perform: makeScreenshotFunc)
.onAppear {
image = image ?? generateImage(uri, tintColor: tintColor)
}
.task { image = await generateImage(uri, tintColor: tintColor) }
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
@@ -89,7 +89,7 @@ private func qrCodeImage(_ image: UIImage) -> some View {
.textSelection(.enabled)
}
private func generateImage(_ uri: String, tintColor: UIColor) -> UIImage? {
private func generateImage(_ uri: String, tintColor: UIColor) async -> UIImage? {
let context = CIContext()
let filter = CIFilter.qrCodeGenerator()
filter.message = Data(uri.utf8)
@@ -467,6 +467,39 @@ private let versionDescriptions: [VersionDescription] = [
)
]
),
VersionDescription(
version: "v6.1",
post: URL(string: "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html"),
features: [
FeatureDescription(
icon: "checkmark.shield",
title: "Better security ✅",
description: "SimpleX protocols reviewed by Trail of Bits."
),
FeatureDescription(
icon: "video",
title: "Better calls",
description: "Switch audio and video during the call."
),
FeatureDescription(
icon: "bolt",
title: "Better notifications",
description: "Improved delivery, reduced traffic usage.\nMore improvements are coming soon!"
),
FeatureDescription(
icon: nil,
title: "Better user experience",
description: nil,
subfeatures: [
("link", "Switch chat profile for 1-time invitations."),
("message", "Customizable message shape."),
("calendar", "Better message dates."),
("arrowshape.turn.up.right", "Forward up to 20 messages at once."),
("flag", "Delete or moderate up to 200 messages.")
]
),
]
),
]
private let lastVersion = versionDescriptions.last!.version
@@ -14,7 +14,6 @@ struct ConnectDesktopView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
var viaSettings = false
@AppStorage(DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS) private var deviceName = UIDevice.current.name
@AppStorage(DEFAULT_CONFIRM_REMOTE_SESSIONS) private var confirmRemoteSessions = false
@AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) private var connectRemoteViaMulticast = true
@@ -57,23 +56,6 @@ struct ConnectDesktopView: View {
}
var body: some View {
if viaSettings {
viewBody
.modifier(BackButton(label: "Back", disabled: Binding.constant(false)) {
if m.activeRemoteCtrl {
alert = .disconnectDesktop(action: .back)
} else {
dismiss()
}
})
} else {
NavigationView {
viewBody
}
}
}
var viewBody: some View {
Group {
let discovery = m.remoteCtrlSession?.discovery
if discovery == true || (discovery == nil && !showConnectScreen) {
+1 -1
View File
@@ -160,7 +160,7 @@ struct TerminalView_Previews: PreviewProvider {
let chatModel = ChatModel()
chatModel.terminalItems = [
.resp(.now, ChatResponse.response(type: "contactSubscribed", json: "{}")),
.resp(.now, ChatResponse.response(type: "newChatItem", json: "{}"))
.resp(.now, ChatResponse.response(type: "newChatItems", json: "{}"))
]
return NavigationView {
TerminalView()
@@ -36,6 +36,10 @@ struct AdvancedNetworkSettings: View {
@State private var showSettingsAlert: NetworkSettingsAlert?
@State private var onionHosts: OnionHosts = .no
@State private var showSaveDialog = false
@State private var netProxy = networkProxyDefault.get()
@State private var currentNetProxy = networkProxyDefault.get()
@State private var useNetProxy = false
@State private var netProxyAuth = false
var body: some View {
VStack {
@@ -102,6 +106,76 @@ struct AdvancedNetworkSettings: View {
.foregroundColor(theme.colors.secondary)
}
Section {
Toggle("Use SOCKS proxy", isOn: $useNetProxy)
Group {
TextField("IP address", text: $netProxy.host)
TextField(
"Port",
text: Binding(
get: { netProxy.port > 0 ? "\(netProxy.port)" : "" },
set: { s in
netProxy.port = if let port = Int(s), port > 0 {
port
} else {
0
}
}
)
)
Toggle("Proxy requires password", isOn: $netProxyAuth)
if netProxyAuth {
TextField("Username", text: $netProxy.username)
PassphraseField(
key: $netProxy.password,
placeholder: "Password",
valid: NetworkProxy.validCredential(netProxy.password)
)
}
}
.if(!useNetProxy) { $0.foregroundColor(theme.colors.secondary) }
.disabled(!useNetProxy)
} header: {
HStack {
Text("SOCKS proxy").foregroundColor(theme.colors.secondary)
if useNetProxy && !netProxy.valid {
Spacer()
Image(systemName: "exclamationmark.circle.fill").foregroundColor(.red)
}
}
} footer: {
if netProxyAuth {
Text("Your credentials may be sent unencrypted.")
.foregroundColor(theme.colors.secondary)
} else {
Text("Do not use credentials with proxy.")
.foregroundColor(theme.colors.secondary)
}
}
.onChange(of: useNetProxy) { useNetProxy in
netCfg.socksProxy = useNetProxy && currentNetProxy.valid
? currentNetProxy.toProxyString()
: nil
netProxy = currentNetProxy
netProxyAuth = netProxy.username != "" || netProxy.password != ""
}
.onChange(of: netProxyAuth) { netProxyAuth in
if netProxyAuth {
netProxy.auth = currentNetProxy.auth
netProxy.username = currentNetProxy.username
netProxy.password = currentNetProxy.password
} else {
netProxy.auth = .username
netProxy.username = ""
netProxy.password = ""
}
}
.onChange(of: netProxy) { netProxy in
netCfg.socksProxy = useNetProxy && netProxy.valid
? netProxy.toProxyString()
: nil
}
Section {
Picker("Use .onion hosts", selection: $onionHosts) {
ForEach(OnionHosts.values, id: \.self) { Text($0.text) }
@@ -122,11 +196,14 @@ struct AdvancedNetworkSettings: View {
if developerTools {
Section {
Picker("Transport isolation", selection: $netCfg.sessionMode) {
ForEach(TransportSessionMode.values, id: \.self) { Text($0.text) }
let modes = TransportSessionMode.values.contains(netCfg.sessionMode)
? TransportSessionMode.values
: TransportSessionMode.values + [netCfg.sessionMode]
ForEach(modes, id: \.self) { Text($0.text) }
}
.frame(height: 36)
} footer: {
Text(sessionModeInfo(netCfg.sessionMode))
sessionModeInfo(netCfg.sessionMode)
.foregroundColor(theme.colors.secondary)
}
}
@@ -156,19 +233,19 @@ struct AdvancedNetworkSettings: View {
Section {
Button("Reset to defaults") {
updateNetCfgView(NetCfg.defaults)
updateNetCfgView(NetCfg.defaults, NetworkProxy.def)
}
.disabled(netCfg == NetCfg.defaults)
Button("Set timeouts for proxy/VPN") {
updateNetCfgView(netCfg.withProxyTimeouts)
updateNetCfgView(netCfg.withProxyTimeouts, netProxy)
}
.disabled(netCfg.hasProxyTimeouts)
Button("Save and reconnect") {
showSettingsAlert = .update
}
.disabled(netCfg == currentNetCfg)
.disabled(netCfg == currentNetCfg || (useNetProxy && !netProxy.valid))
}
}
}
@@ -182,7 +259,8 @@ struct AdvancedNetworkSettings: View {
if cfgLoaded { return }
cfgLoaded = true
currentNetCfg = getNetCfg()
updateNetCfgView(currentNetCfg)
currentNetProxy = networkProxyDefault.get()
updateNetCfgView(currentNetCfg, currentNetProxy)
}
.alert(item: $showSettingsAlert) { a in
switch a {
@@ -206,7 +284,7 @@ struct AdvancedNetworkSettings: View {
if netCfg == currentNetCfg {
dismiss()
cfgLoaded = false
} else {
} else if !useNetProxy || netProxy.valid {
showSaveDialog = true
}
})
@@ -221,18 +299,26 @@ struct AdvancedNetworkSettings: View {
}
}
private func updateNetCfgView(_ cfg: NetCfg) {
private func updateNetCfgView(_ cfg: NetCfg, _ proxy: NetworkProxy) {
netCfg = cfg
netProxy = proxy
onionHosts = OnionHosts(netCfg: netCfg)
enableKeepAlive = netCfg.enableKeepAlive
keepAliveOpts = netCfg.tcpKeepAlive ?? KeepAliveOpts.defaults
useNetProxy = netCfg.socksProxy != nil
netProxyAuth = switch netProxy.auth {
case .username: netProxy.username != "" || netProxy.password != ""
case .isolate: false
}
}
private func saveNetCfg() -> Bool {
do {
try setNetworkConfig(netCfg)
currentNetCfg = netCfg
setNetCfg(netCfg)
setNetCfg(netCfg, networkProxy: useNetProxy ? netProxy : nil)
currentNetProxy = netProxy
networkProxyDefault.set(netProxy)
return true
} catch let error {
let err = responseError(error)
@@ -270,10 +356,13 @@ struct AdvancedNetworkSettings: View {
}
}
private func sessionModeInfo(_ mode: TransportSessionMode) -> LocalizedStringKey {
switch mode {
case .user: return "A separate TCP connection will be used **for each chat profile you have in the app**."
case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail."
private func sessionModeInfo(_ mode: TransportSessionMode) -> Text {
let userMode = Text("A separate TCP connection will be used **for each chat profile you have in the app**.")
return switch mode {
case .user: userMode
case .session: userMode + Text("\n") + Text("New SOCKS credentials will be used every time you start the app.")
case .server: userMode + Text("\n") + Text("New SOCKS credentials will be used for each server.")
case .entity: Text("A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.")
}
}
@@ -19,9 +19,15 @@ extension AppSettings {
val.hostMode = .publicHost
val.requiredHostMode = true
}
val.socksProxy = nil
setNetCfg(val)
if val.socksProxy != nil {
val.socksProxy = networkProxy?.toProxyString()
setNetCfg(val, networkProxy: networkProxy)
} else {
val.socksProxy = nil
setNetCfg(val, networkProxy: nil)
}
}
if let val = networkProxy { networkProxyDefault.set(val) }
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) }
if let val = privacyAcceptImages {
@@ -52,10 +58,12 @@ extension AppSettings {
profileImageCornerRadiusGroupDefault.set(val)
def.setValue(val, forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
}
if let val = uiColorScheme { def.setValue(val, forKey: DEFAULT_CURRENT_THEME) }
if let val = uiDarkColorScheme { def.setValue(val, forKey: DEFAULT_SYSTEM_DARK_THEME) }
if let val = uiCurrentThemeIds { def.setValue(val, forKey: DEFAULT_CURRENT_THEME_IDS) }
if let val = uiThemes { def.setValue(val.skipDuplicates(), forKey: DEFAULT_THEME_OVERRIDES) }
if let val = uiChatItemRoundness { def.setValue(val, forKey: DEFAULT_CHAT_ITEM_ROUNDNESS)}
if let val = uiChatItemTail { def.setValue(val, forKey: DEFAULT_CHAT_ITEM_TAIL)}
if let val = uiColorScheme { currentThemeDefault.set(val) }
if let val = uiDarkColorScheme { systemDarkThemeDefault.set(val) }
if let val = uiCurrentThemeIds { currentThemeIdsDefault.set(val) }
if let val = uiThemes { themeOverridesDefault.set(val.skipDuplicates()) }
if let val = oneHandUI { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_ONE_HAND_UI) }
}
@@ -63,6 +71,7 @@ extension AppSettings {
let def = UserDefaults.standard
var c = AppSettings.defaults
c.networkConfig = getNetCfg()
c.networkProxy = networkProxyDefault.get()
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get()
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
@@ -84,6 +93,8 @@ extension AppSettings {
c.iosCallKitEnabled = callKitEnabledGroupDefault.get()
c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
c.uiProfileImageCornerRadius = def.double(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
c.uiChatItemRoundness = def.double(forKey: DEFAULT_CHAT_ITEM_ROUNDNESS)
c.uiChatItemTail = def.bool(forKey: DEFAULT_CHAT_ITEM_TAIL)
c.uiColorScheme = currentThemeDefault.get()
c.uiDarkColorScheme = systemDarkThemeDefault.get()
c.uiCurrentThemeIds = currentThemeIdsDefault.get()
@@ -33,6 +33,8 @@ struct AppearanceSettings: View {
}()
@State private var darkModeTheme: String = UserDefaults.standard.string(forKey: DEFAULT_SYSTEM_DARK_THEME) ?? DefaultTheme.DARK.themeName
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
@AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var chatItemRoundness = defaultChatItemRoundness
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var chatItemTail = true
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
@@ -179,6 +181,14 @@ struct AppearanceSettings: View {
}
}
Section(header: Text("Message shape").foregroundColor(theme.colors.secondary)) {
HStack {
Text("Corner")
Slider(value: $chatItemRoundness, in: 0...1, step: 0.05)
}
Toggle("Tail", isOn: $chatItemTail)
}
Section(header: Text("Profile images").foregroundColor(theme.colors.secondary)) {
HStack(spacing: 16) {
if let img = m.currentUser?.image, img != "" {
@@ -358,20 +368,21 @@ struct ChatThemePreview: View {
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 {
ChatItemView(chat: Chat.sampleData, chatItem: alice, revealed: Binding.constant(false))
.modifier(ChatItemClipped())
.modifier(ChatItemClipped(alice, tailVisible: true))
Spacer()
}
HStack {
Spacer()
ChatItemView(chat: Chat.sampleData, chatItem: bob, revealed: Binding.constant(false))
.modifier(ChatItemClipped())
.modifier(ChatItemClipped(bob, tailVisible: true))
.frame(alignment: .trailing)
}
} else {
Rectangle().fill(.clear)
}
}
.padding(10)
.padding(.vertical, 10)
.padding(.horizontal, 16)
.frame(maxWidth: .infinity)
if let wallpaperType, let wallpaperImage = wallpaperType.image, let backgroundColor, let tintColor {
@@ -797,7 +808,7 @@ struct ThemeDestinationPicker: View {
@Binding var customizeThemeIsOpen: Bool
var body: some View {
let values = [(nil, "All profiles")] + m.users.filter { $0.user.activeUser }.map { ($0.user.userId, $0.user.chatViewName)}
let values = [(nil, NSLocalizedString("All profiles", comment: "profile dropdown"))] + m.users.filter { $0.user.activeUser }.map { ($0.user.userId, $0.user.chatViewName)}
if values.contains(where: { (userId, text) in userId == themeUserDestination?.0 }) {
Picker("Apply to", selection: $themeUserDest) {
@@ -26,6 +26,7 @@ struct IncognitoHelp: View {
Text("Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).")
}
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.modifier(ThemedBackground())
}
@@ -32,6 +32,17 @@ struct PreferencesView: View {
.disabled(currentPreferences == preferences)
}
}
.onDisappear {
if currentPreferences != preferences {
showAlert(
title: NSLocalizedString("Your chat preferences", comment: "alert title"),
message: NSLocalizedString("Chat preferences were changed.", comment: "alert message"),
buttonTitle: NSLocalizedString("Save", comment: "alert button"),
buttonAction: savePreferences,
cancelButton: true
)
}
}
}
private func featureSection(_ feature: ChatFeature, _ allowFeature: Binding<FeatureAllowed>) -> some View {
@@ -47,6 +47,8 @@ let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen" // deprecated, only used for
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue" // deprecated, only used for migration
let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle" // deprecated, only used for migration
let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
let DEFAULT_CHAT_ITEM_ROUNDNESS = "chatItemRoundness"
let DEFAULT_CHAT_ITEM_TAIL = "chatItemTail"
let DEFAULT_ONE_HAND_UI_CARD_SHOWN = "oneHandUICardShown"
let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial"
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
@@ -73,8 +75,12 @@ let DEFAULT_SYSTEM_DARK_THEME = "systemDarkTheme"
let DEFAULT_CURRENT_THEME_IDS = "currentThemeIds"
let DEFAULT_THEME_OVERRIDES = "themeOverrides"
let DEFAULT_NETWORK_PROXY = "networkProxy"
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
let defaultChatItemRoundness: Double = 0.75
let appDefaults: [String: Any] = [
DEFAULT_SHOW_LA_NOTICE: false,
DEFAULT_LA_NOTICE_SHOWN: false,
@@ -98,6 +104,8 @@ let appDefaults: [String: Any] = [
DEFAULT_DEVELOPER_TOOLS: false,
DEFAULT_ENCRYPTION_STARTED: false,
DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
DEFAULT_CHAT_ITEM_ROUNDNESS: defaultChatItemRoundness,
DEFAULT_CHAT_ITEM_TAIL: true,
DEFAULT_ONE_HAND_UI_CARD_SHOWN: false,
DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial,
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
@@ -245,13 +253,14 @@ public class CodableDefault<T: Codable> {
}
}
let networkProxyDefault: CodableDefault<NetworkProxy> = CodableDefault(defaults: UserDefaults.standard, forKey: DEFAULT_NETWORK_PROXY, withDefault: NetworkProxy.def)
struct SettingsView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var sceneDelegate: SceneDelegate
@EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool
@State private var showProgress: Bool = false
var body: some View {
@@ -260,71 +269,12 @@ struct SettingsView: View {
if showProgress {
progressView()
}
if let la = chatModel.laRequest {
LocalAuthView(authRequest: la)
}
}
}
@ViewBuilder func settingsView() -> some View {
let user = chatModel.currentUser
NavigationView {
List {
Section(header: Text("You").foregroundColor(theme.colors.secondary)) {
if let user = user {
NavigationLink {
UserProfile()
.navigationTitle("Your current profile")
.modifier(ThemedBackground())
} label: {
ProfilePreview(profileOf: user)
.padding(.leading, -8)
}
}
NavigationLink {
UserProfilesView(showSettings: $showSettings)
} label: {
settingsRow("person.crop.rectangle.stack", color: theme.colors.secondary) { Text("Your chat profiles") }
}
if let user = user {
NavigationLink {
UserAddressView(shareViaProfile: user.addressShared)
.navigationTitle("SimpleX address")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("qrcode", color: theme.colors.secondary) { Text("Your SimpleX address") }
}
NavigationLink {
PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences)
.navigationTitle("Your preferences")
.modifier(ThemedBackground(grouped: true))
} label: {
settingsRow("switch.2", color: theme.colors.secondary) { Text("Chat preferences") }
}
}
NavigationLink {
ConnectDesktopView(viaSettings: true)
} label: {
settingsRow("desktopcomputer", color: theme.colors.secondary) { Text("Use from desktop") }
}
NavigationLink {
MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress)
.navigationTitle("Migrate device")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") }
}
}
.disabled(chatModel.chatRunning != true)
Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) {
NavigationLink {
NotificationsView()
@@ -375,14 +325,24 @@ struct SettingsView: View {
}
.disabled(chatModel.chatRunning != true)
}
chatDatabaseRow()
}
Section(header: Text("Chat database").foregroundColor(theme.colors.secondary)) {
chatDatabaseRow()
NavigationLink {
MigrateFromDevice(showProgressOnSettings: $showProgress)
.navigationTitle("Migrate device")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") }
}
}
Section(header: Text("Help").foregroundColor(theme.colors.secondary)) {
if let user = user {
NavigationLink {
ChatHelp(showSettings: $showSettings)
ChatHelp(dismissSettingsSheet: dismiss)
.navigationTitle("Welcome \(user.displayName)!")
.modifier(ThemedBackground())
.frame(maxHeight: .infinity, alignment: .top)
@@ -407,7 +367,7 @@ struct SettingsView: View {
}
settingsRow("number", color: theme.colors.secondary) {
Button("Send questions and ideas") {
showSettings = false
dismiss()
DispatchQueue.main.async {
UIApplication.shared.open(simplexTeamURL)
}
@@ -456,16 +416,15 @@ struct SettingsView: View {
}
.navigationTitle("Your settings")
.modifier(ThemedBackground(grouped: true))
}
.onDisappear {
chatModel.showingTerminal = false
chatModel.terminalItems = []
}
.onDisappear {
chatModel.showingTerminal = false
chatModel.terminalItems = []
}
}
private func chatDatabaseRow() -> some View {
NavigationLink {
DatabaseView(showSettings: $showSettings, chatItemTTL: chatModel.chatItemTTL)
DatabaseView(dismissSettingsSheet: dismiss, chatItemTTL: chatModel.chatItemTTL)
.navigationTitle("Your chat database")
.modifier(ThemedBackground(grouped: true))
} label: {
@@ -543,26 +502,25 @@ struct ProfilePreview: View {
HStack {
ProfileImage(imageStr: profileOf.image, size: 44, color: color)
.padding(.trailing, 6)
.padding(.vertical, 6)
VStack(alignment: .leading) {
Text(profileOf.displayName)
.fontWeight(.bold)
.font(.title2)
if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName {
Text(profileOf.fullName)
}
}
profileName(profileOf).lineLimit(1)
}
}
}
func profileName(_ profileOf: NamedChat) -> Text {
var t = Text(profileOf.displayName).fontWeight(.semibold).font(.title2)
if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName {
t = t + Text(" (" + profileOf.fullName + ")")
// .font(.callout)
}
return t
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.currentUser = User.sampleData
@State var showSettings = false
return SettingsView(showSettings: $showSettings)
return SettingsView()
.environmentObject(chatModel)
}
}
@@ -14,7 +14,6 @@ struct UserAddressView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject private var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@State var viaCreateLinkView = false
@State var shareViaProfile = false
@State private var aas = AutoAcceptState()
@State private var savedAAS = AutoAcceptState()
@@ -22,7 +21,6 @@ struct UserAddressView: View {
@State private var showMailView = false
@State private var mailViewResult: Result<MFMailComposeResult, Error>? = nil
@State private var alert: UserAddressAlert?
@State private var showSaveDialogue = false
@State private var progressIndicator = false
@FocusState private var keyboardVisible: Bool
@@ -44,26 +42,19 @@ struct UserAddressView: View {
var body: some View {
ZStack {
if viaCreateLinkView {
userAddressScrollView()
} else {
userAddressScrollView()
.modifier(BackButton(disabled: Binding.constant(false)) {
if savedAAS == aas {
dismiss()
} else {
keyboardVisible = false
showSaveDialogue = true
}
})
.confirmationDialog("Save settings?", isPresented: $showSaveDialogue) {
Button("Save auto-accept settings") {
saveAAS()
dismiss()
}
Button("Exit without saving") { dismiss() }
userAddressScrollView()
.onDisappear {
if savedAAS != aas {
showAlert(
title: NSLocalizedString("Auto-accept settings", comment: "alert title"),
message: NSLocalizedString("Settings were changed.", comment: "alert message"),
buttonTitle: NSLocalizedString("Save", comment: "alert button"),
buttonAction: saveAAS,
cancelButton: true
)
}
}
}
if progressIndicator {
ZStack {
if chatModel.userAddress != nil {
@@ -342,7 +333,7 @@ struct UserAddressView: View {
}
}
}
private struct AutoAcceptState: Equatable {
var enable = false
var incognito = false
@@ -447,6 +438,8 @@ struct UserAddressView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.userAddress = UserContactLink(connReqContact: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")
return Group {
UserAddressView()
.environmentObject(chatModel)
File diff suppressed because one or more lines are too long
@@ -9,7 +9,6 @@ import SimpleXChat
struct UserProfilesView: View {
@EnvironmentObject private var m: ChatModel
@EnvironmentObject private var theme: AppTheme
@Binding var showSettings: Bool
@Environment(\.editMode) private var editMode
@AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true
@AppStorage(DEFAULT_SHOW_MUTE_PROFILE_ALERT) private var showMuteProfileAlert = true
@@ -96,8 +95,7 @@ struct UserProfilesView: View {
} label: {
Label("Add profile", systemImage: "plus")
}
.frame(height: 44)
.padding(.vertical, 4)
.frame(height: 38)
}
} footer: {
Text("Tap to activate profile.")
@@ -285,7 +283,7 @@ struct UserProfilesView: View {
await MainActor.run {
onboardingStageDefault.set(.step1_SimpleXInfo)
m.onboardingStage = .step1_SimpleXInfo
showSettings = false
dismissAllSheets()
}
}
} else {
@@ -308,14 +306,14 @@ struct UserProfilesView: View {
Task {
do {
try await changeActiveUserAsync_(user.userId, viewPwd: userViewPassword(user))
dismissAllSheets()
} catch {
await MainActor.run { alert = .activateUserError(error: responseError(error)) }
}
}
} label: {
HStack {
ProfileImage(imageStr: user.image, size: 44)
.padding(.vertical, 4)
ProfileImage(imageStr: user.image, size: 38)
.padding(.trailing, 12)
Text(user.chatViewName)
Spacer()
@@ -406,8 +404,15 @@ public func chatPasswordHash(_ pwd: String, _ salt: String) -> String {
return hash
}
public func correctPassword(_ user: User, _ pwd: String) -> Bool {
if let ph = user.viewPwdHash {
return pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash
}
return false
}
struct UserProfilesView_Previews: PreviewProvider {
static var previews: some View {
UserProfilesView(showSettings: Binding.constant(true))
UserProfilesView()
}
}
@@ -137,6 +137,10 @@
<target>%@ иска да се свърже!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ и %lld членове</target>
@@ -157,11 +161,31 @@
<target>%d дни</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve">
<source>%d file(s) are still being downloaded.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) failed to download." xml:space="preserve">
<source>%d file(s) failed to download.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were deleted." xml:space="preserve">
<source>%d file(s) were deleted.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve">
<source>%d file(s) were not downloaded.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d hours" xml:space="preserve">
<source>%d hours</source>
<target>%d часа</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d messages not forwarded" xml:space="preserve">
<source>%d messages not forwarded</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="%d min" xml:space="preserve">
<source>%d min</source>
<target>%d мин.</target>
@@ -713,7 +737,7 @@
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<note>No comment provided by engineer.</note>
<note>profile dropdown</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source>
@@ -901,6 +925,10 @@
<target>Кода за достъп до приложение се заменя с код за самоунищожение.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App session" xml:space="preserve">
<source>App session</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App version" xml:space="preserve">
<source>App version</source>
<target>Версия на приложението</target>
@@ -1003,6 +1031,10 @@
<target>Автоматично приемане на изображения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
<source>Back</source>
<target>Назад</target>
@@ -1027,11 +1059,19 @@
<target>Лош хеш на съобщението</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<target>По-добри групи</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>По-добри съобщения</target>
@@ -1041,6 +1081,18 @@
<source>Better networking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Black" xml:space="preserve">
<source>Black</source>
<note>No comment provided by engineer.</note>
@@ -1167,7 +1219,7 @@
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Отказ</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve">
<source>Cancel migration</source>
@@ -1186,7 +1238,7 @@
<trans-unit id="Cannot receive file" xml:space="preserve">
<source>Cannot receive file</source>
<target>Файлът не може да бъде получен</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
@@ -1310,6 +1362,15 @@
<target>Чат настройки</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences were changed." xml:space="preserve">
<source>Chat preferences were changed.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Chat profile" xml:space="preserve">
<source>Chat profile</source>
<target>Потребителски профил</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat theme" xml:space="preserve">
<source>Chat theme</source>
<note>No comment provided by engineer.</note>
@@ -1685,6 +1746,10 @@ This is your own one-time link!</source>
<target>Версия на ядрото: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Поправи име на %@?</target>
@@ -1808,6 +1873,10 @@ This is your own one-time link!</source>
<target>Персонализирано време</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customize theme" xml:space="preserve">
<source>Customize theme</source>
<note>No comment provided by engineer.</note>
@@ -2097,6 +2166,10 @@ This is your own one-time link!</source>
<target>Изтрий старата база данни?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve">
<source>Delete pending connection?</source>
<target>Изтрий предстоящата връзка?</target>
@@ -2334,6 +2407,10 @@ This is your own one-time link!</source>
<target>Не изпращай история на нови членове.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not use credentials with proxy." xml:space="preserve">
<source>Do not use credentials with proxy.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>Не създавай адрес</target>
@@ -2357,7 +2434,8 @@ This is your own one-time link!</source>
<trans-unit id="Download" xml:space="preserve">
<source>Download</source>
<target>Изтегли</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Download errors" xml:space="preserve">
<source>Download errors</source>
@@ -2373,6 +2451,10 @@ This is your own one-time link!</source>
<target>Свали файл</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Download files" xml:space="preserve">
<source>Download files</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Downloaded" xml:space="preserve">
<source>Downloaded</source>
<note>No comment provided by engineer.</note>
@@ -2645,6 +2727,10 @@ This is your own one-time link!</source>
<target>Грешка при промяна на адреса</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Грешка при промяна на ролята</target>
@@ -2655,6 +2741,10 @@ This is your own one-time link!</source>
<target>Грешка при промяна на настройката</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -2773,6 +2863,10 @@ This is your own one-time link!</source>
<target>Грешка при зареждане на %@ сървъри</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error migrating settings" xml:space="preserve">
<source>Error migrating settings</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<target>Грешка при отваряне на чата</target>
@@ -2781,7 +2875,7 @@ This is your own one-time link!</source>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Грешка при получаване на файл</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Error reconnecting server" xml:space="preserve">
<source>Error reconnecting server</source>
@@ -2870,10 +2964,14 @@ This is your own one-time link!</source>
<target>Грешка при спиране на чата</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Грешка при смяна на профил!</target>
<note>No comment provided by engineer.</note>
<note>alertTitle</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
@@ -2918,8 +3016,7 @@ This is your own one-time link!</source>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>Грешка: %@</target>
<note>file error text
snd error text</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Error: URL is invalid" xml:space="preserve">
<source>Error: URL is invalid</source>
@@ -3003,6 +3100,11 @@ This is your own one-time link!</source>
<source>File error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File errors:&#10;%@" xml:space="preserve">
<source>File errors:
%@</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve">
<source>File not found - most likely file was deleted or cancelled.</source>
<note>file error text</note>
@@ -3133,11 +3235,27 @@ This is your own one-time link!</source>
<target>Препрати</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Forward %d message(s)?" xml:space="preserve">
<source>Forward %d message(s)?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Forward and save messages" xml:space="preserve">
<source>Forward and save messages</source>
<target>Препращане и запазване на съобщения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forward messages" xml:space="preserve">
<source>Forward messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Forward messages without files?" xml:space="preserve">
<source>Forward messages without files?</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarded" xml:space="preserve">
<source>Forwarded</source>
<target>Препратено</target>
@@ -3148,6 +3266,10 @@ This is your own one-time link!</source>
<target>Препратено от</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding %lld messages" xml:space="preserve">
<source>Forwarding %lld messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve">
<source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -3190,11 +3312,6 @@ Error: %2$@</source>
<target>Пълно име (незадължително)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Full name:" xml:space="preserve">
<source>Full name:</source>
<target>Пълно име:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<target>Напълно децентрализирана – видима е само за членовете.</target>
@@ -3438,6 +3555,10 @@ Error: %2$@</source>
<target>ICE сървъри (по един на ред)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="IP address" xml:space="preserve">
<source>IP address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
<target>Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка.</target>
@@ -3512,6 +3633,11 @@ Error: %2$@</source>
<target>Импортиране на архив</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<target>Подобрена доставка на съобщения</target>
@@ -4069,6 +4195,10 @@ This is your link for group %@!</source>
<source>Message servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Източникът на съобщението остава скрит.</target>
@@ -4115,6 +4245,10 @@ This is your link for group %@!</source>
<source>Messages sent</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages were deleted after you selected them." xml:space="preserve">
<source>Messages were deleted after you selected them.</source>
<note>alert message</note>
</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">
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
<target>Съобщенията, файловете и разговорите са защитени чрез **криптиране от край до край** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом.</target>
@@ -4269,6 +4403,14 @@ This is your link for group %@!</source>
<target>Нов kод за достъп</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve">
<source>New SOCKS credentials will be used every time you start the app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve">
<source>New SOCKS credentials will be used for each server.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<target>Нов чат</target>
@@ -4385,6 +4527,14 @@ This is your link for group %@!</source>
<target>Няма мрежова връзка</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record speech" xml:space="preserve">
<source>No permission to record speech</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record video" xml:space="preserve">
<source>No permission to record video</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>Няма разрешение за запис на гласово съобщение</target>
@@ -4404,6 +4554,10 @@ This is your link for group %@!</source>
<source>Nothing selected</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Nothing to forward!" xml:space="preserve">
<source>Nothing to forward!</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Известия</target>
@@ -4436,7 +4590,7 @@ This is your link for group %@!</source>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ок</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Old database" xml:space="preserve">
<source>Old database</source>
@@ -4624,6 +4778,11 @@ Requires compatible VPN.</source>
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other file errors:&#10;%@" xml:space="preserve">
<source>Other file errors:
%@</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>PING бройка</target>
@@ -4659,6 +4818,10 @@ Requires compatible VPN.</source>
<target>Кодът за достъп е зададен!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password" xml:space="preserve">
<source>Password</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password to show" xml:space="preserve">
<source>Password to show</source>
<target>Парола за показване</target>
@@ -4803,6 +4966,10 @@ Error: %@</source>
<target>Полски интерфейс</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Port" xml:space="preserve">
<source>Port</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve">
<source>Possibly, certificate fingerprint in server address is incorrect</source>
<target>Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен</target>
@@ -4883,16 +5050,6 @@ Error: %@</source>
<target>Профилни изображения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<target>Име на профила</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<target>Име на профила:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source>
<target>Профилна парола</target>
@@ -4989,6 +5146,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Proxied servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Proxy requires password" xml:space="preserve">
<source>Proxy requires password</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>Push известия</target>
@@ -5200,6 +5361,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Премахване</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove image" xml:space="preserve">
<source>Remove image</source>
<note>No comment provided by engineer.</note>
@@ -5373,6 +5538,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>SMP server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SOCKS proxy" xml:space="preserve">
<source>SOCKS proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Safely receive files" xml:space="preserve">
<source>Safely receive files</source>
<note>No comment provided by engineer.</note>
@@ -5385,12 +5554,13 @@ Enable in *Network &amp; servers* settings.</source>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<target>Запази</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
<source>Save (and notify contacts)</source>
<target>Запази (и уведоми контактите)</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Save and notify contact" xml:space="preserve">
<source>Save and notify contact</source>
@@ -5416,11 +5586,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Запази архив</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save auto-accept settings" xml:space="preserve">
<source>Save auto-accept settings</source>
<target>Запази настройките за автоматично приемане</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save group profile" xml:space="preserve">
<source>Save group profile</source>
<target>Запази профила на групата</target>
@@ -5456,16 +5621,15 @@ Enable in *Network &amp; servers* settings.</source>
<target>Запази сървърите?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save settings?" xml:space="preserve">
<source>Save settings?</source>
<target>Запази настройките?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save welcome message?" xml:space="preserve">
<source>Save welcome message?</source>
<target>Запази съобщението при посрещане?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save your profile?" xml:space="preserve">
<source>Save your profile?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Saved" xml:space="preserve">
<source>Saved</source>
<target>Запазено</target>
@@ -5486,6 +5650,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Запазено съобщение</target>
<note>message info title</note>
</trans-unit>
<trans-unit id="Saving %lld messages" xml:space="preserve">
<source>Saving %lld messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scale" xml:space="preserve">
<source>Scale</source>
<note>No comment provided by engineer.</note>
@@ -5562,6 +5730,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Избери</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<note>No comment provided by engineer.</note>
@@ -5674,7 +5846,7 @@ Enable in *Network &amp; servers* settings.</source>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>Подателят отмени прехвърлянето на файла.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Sender may have deleted the connection request." xml:space="preserve">
<source>Sender may have deleted the connection request.</source>
@@ -5766,6 +5938,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Sent via proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server address" xml:space="preserve">
<source>Server address</source>
<note>No comment provided by engineer.</note>
@@ -5877,6 +6053,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Настройки</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Settings were changed." xml:space="preserve">
<source>Settings were changed.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Shape profile images" xml:space="preserve">
<source>Shape profile images</source>
<target>Променете формата на профилните изображения</target>
@@ -5911,6 +6091,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Сподели линк</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Сподели този еднократен линк за връзка</target>
@@ -6041,6 +6225,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Еднократна покана за SimpleX</target>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Опростен режим инкогнито</target>
@@ -6069,6 +6257,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Soft</source>
<note>blur media</note>
</trans-unit>
<trans-unit id="Some app settings were not migrated." xml:space="preserve">
<source>Some app settings were not migrated.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some file(s) were not exported:" xml:space="preserve">
<source>Some file(s) were not exported:</source>
<note>No comment provided by engineer.</note>
@@ -6201,6 +6393,14 @@ Enable in *Network &amp; servers* settings.</source>
<target>Подкрепете SimpleX Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>Системен</target>
@@ -6235,6 +6435,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Направи снимка</target>
@@ -6423,6 +6627,10 @@ It can happen because of some bug or when the connection is compromised.</source
<target>Текстът, който поставихте, не е SimpleX линк за връзка.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve">
<source>The uploaded database archive will be permanently removed from the servers.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Themes" xml:space="preserve">
<source>Themes</source>
<note>No comment provided by engineer.</note>
@@ -6546,6 +6754,14 @@ You will be prompted to complete authentication before this feature is enabled.<
Ще бъдете подканени да извършите идентификация, преди тази функция да бъде активирана.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve">
<source>To record speech please grant permission to use Microphone.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record video please grant permission to use Camera." xml:space="preserve">
<source>To record video please grant permission to use Camera.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve">
<source>To record voice message please grant permission to use Microphone.</source>
<target>За да запишете гласово съобщение, моля, дайте разрешение за използване на микрофон.</target>
@@ -6694,7 +6910,7 @@ You will be prompted to complete authentication before this feature is enabled.<
</trans-unit>
<trans-unit id="Unknown servers!" xml:space="preserve">
<source>Unknown servers!</source>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
@@ -6804,6 +7020,10 @@ To connect, please ask your contact to create another connection link and check
<target>Използвай .onion хостове</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SOCKS proxy" xml:space="preserve">
<source>Use SOCKS proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
<source>Use SimpleX Chat servers?</source>
<target>Използвай сървърите на SimpleX Chat?</target>
@@ -6866,15 +7086,14 @@ To connect, please ask your contact to create another connection link and check
<source>Use the app with one hand.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User profile" xml:space="preserve">
<source>User profile</source>
<target>Потребителски профил</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User selection" xml:space="preserve">
<source>User selection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
<source>Using SimpleX Chat servers.</source>
<target>Използват се сървърите на SimpleX Chat.</target>
@@ -7104,7 +7323,7 @@ To connect, please ask your contact to create another connection link and check
</trans-unit>
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
@@ -7128,11 +7347,6 @@ To connect, please ask your contact to create another connection link and check
<source>XFTP server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You" xml:space="preserve">
<source>You</source>
<target>Вие</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
<source>You **must not** use the same database on two devices.</source>
<target>**Не трябва** да използвате една и съща база данни на две устройства.</target>
@@ -7475,11 +7689,19 @@ Repeat connection request?</source>
<target>Вашата чат база данни не е криптирана - задайте парола, за да я криптирате.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat preferences" xml:space="preserve">
<source>Your chat preferences</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>Вашите чат профили</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@).</target>
@@ -7495,6 +7717,10 @@ Repeat connection request?</source>
<target>Вашите контакти ще останат свързани.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve">
<source>Your credentials may be sent unencrypted.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve">
<source>Your current chat database will be DELETED and REPLACED with the imported one.</source>
<target>Вашата текуща чат база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната.</target>
@@ -7525,13 +7751,15 @@ Repeat connection request?</source>
<target>Вашият профил **%@** ще бъде споделен.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
<target>Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти.
SimpleX сървърите не могат да видят вашия профил.</target>
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
<target>Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve">
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство.</target>
@@ -135,6 +135,10 @@
<target>%@ se chce připojit!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<note>No comment provided by engineer.</note>
@@ -154,11 +158,31 @@
<target>%d dní</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve">
<source>%d file(s) are still being downloaded.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) failed to download." xml:space="preserve">
<source>%d file(s) failed to download.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were deleted." xml:space="preserve">
<source>%d file(s) were deleted.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve">
<source>%d file(s) were not downloaded.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d hours" xml:space="preserve">
<source>%d hours</source>
<target>%d hodin</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d messages not forwarded" xml:space="preserve">
<source>%d messages not forwarded</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="%d min" xml:space="preserve">
<source>%d min</source>
<target>%d minuty</target>
@@ -691,7 +715,7 @@
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<note>No comment provided by engineer.</note>
<note>profile dropdown</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source>
@@ -874,6 +898,10 @@
<target>Přístupový kód aplikace je nahrazen sebedestrukčním přístupovým heslem.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App session" xml:space="preserve">
<source>App session</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App version" xml:space="preserve">
<source>App version</source>
<target>Verze aplikace</target>
@@ -973,6 +1001,10 @@
<target>Automaticky přijímat obrázky</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
<source>Back</source>
<target>Zpět</target>
@@ -996,10 +1028,18 @@
<target>Špatný hash zprávy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>Lepší zprávy</target>
@@ -1009,6 +1049,18 @@
<source>Better networking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Black" xml:space="preserve">
<source>Black</source>
<note>No comment provided by engineer.</note>
@@ -1127,7 +1179,7 @@
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Zrušit</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve">
<source>Cancel migration</source>
@@ -1145,7 +1197,7 @@
<trans-unit id="Cannot receive file" xml:space="preserve">
<source>Cannot receive file</source>
<target>Nelze přijmout soubor</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
@@ -1266,6 +1318,15 @@
<target>Předvolby chatu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences were changed." xml:space="preserve">
<source>Chat preferences were changed.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Chat profile" xml:space="preserve">
<source>Chat profile</source>
<target>Profil uživatele</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat theme" xml:space="preserve">
<source>Chat theme</source>
<note>No comment provided by engineer.</note>
@@ -1623,6 +1684,10 @@ This is your own one-time link!</source>
<target>Verze jádra: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
@@ -1738,6 +1803,10 @@ This is your own one-time link!</source>
<target>Vlastní čas</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customize theme" xml:space="preserve">
<source>Customize theme</source>
<note>No comment provided by engineer.</note>
@@ -2024,6 +2093,10 @@ This is your own one-time link!</source>
<target>Smazat starou databázi?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve">
<source>Delete pending connection?</source>
<target>Smazat čekající připojení?</target>
@@ -2255,6 +2328,10 @@ This is your own one-time link!</source>
<source>Do not send history to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not use credentials with proxy." xml:space="preserve">
<source>Do not use credentials with proxy.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>Nevytvářet adresu</target>
@@ -2277,7 +2354,8 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Download" xml:space="preserve">
<source>Download</source>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Download errors" xml:space="preserve">
<source>Download errors</source>
@@ -2292,6 +2370,10 @@ This is your own one-time link!</source>
<target>Stáhnout soubor</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Download files" xml:space="preserve">
<source>Download files</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Downloaded" xml:space="preserve">
<source>Downloaded</source>
<note>No comment provided by engineer.</note>
@@ -2552,6 +2634,10 @@ This is your own one-time link!</source>
<target>Chuba změny adresy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Chyba při změně role</target>
@@ -2562,6 +2648,10 @@ This is your own one-time link!</source>
<target>Chyba změny nastavení</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -2678,6 +2768,10 @@ This is your own one-time link!</source>
<target>Chyba načítání %@ serverů</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error migrating settings" xml:space="preserve">
<source>Error migrating settings</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
@@ -2685,7 +2779,7 @@ This is your own one-time link!</source>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Chyba při příjmu souboru</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Error reconnecting server" xml:space="preserve">
<source>Error reconnecting server</source>
@@ -2772,10 +2866,14 @@ This is your own one-time link!</source>
<target>Chyba při zastavení chatu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Chyba při přepínání profilu!</target>
<note>No comment provided by engineer.</note>
<note>alertTitle</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
@@ -2818,8 +2916,7 @@ This is your own one-time link!</source>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>Chyba: %@</target>
<note>file error text
snd error text</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Error: URL is invalid" xml:space="preserve">
<source>Error: URL is invalid</source>
@@ -2900,6 +2997,11 @@ This is your own one-time link!</source>
<source>File error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File errors:&#10;%@" xml:space="preserve">
<source>File errors:
%@</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve">
<source>File not found - most likely file was deleted or cancelled.</source>
<note>file error text</note>
@@ -3026,10 +3128,26 @@ This is your own one-time link!</source>
<source>Forward</source>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Forward %d message(s)?" xml:space="preserve">
<source>Forward %d message(s)?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Forward and save messages" xml:space="preserve">
<source>Forward and save messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forward messages" xml:space="preserve">
<source>Forward messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Forward messages without files?" xml:space="preserve">
<source>Forward messages without files?</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarded" xml:space="preserve">
<source>Forwarded</source>
<note>No comment provided by engineer.</note>
@@ -3038,6 +3156,10 @@ This is your own one-time link!</source>
<source>Forwarded from</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding %lld messages" xml:space="preserve">
<source>Forwarding %lld messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve">
<source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -3079,11 +3201,6 @@ Error: %2$@</source>
<target>Celé jméno (volitelně)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Full name:" xml:space="preserve">
<source>Full name:</source>
<target>Celé jméno:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<note>No comment provided by engineer.</note>
@@ -3321,6 +3438,10 @@ Error: %2$@</source>
<target>Servery ICE (jeden na řádek)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="IP address" xml:space="preserve">
<source>IP address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
<target>Pokud se nemůžete setkat osobně, zobrazte QR kód ve videohovoru nebo sdílejte odkaz.</target>
@@ -3393,6 +3514,11 @@ Error: %2$@</source>
<source>Importing archive</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<note>No comment provided by engineer.</note>
@@ -3928,6 +4054,10 @@ This is your link for group %@!</source>
<source>Message servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<note>No comment provided by engineer.</note>
@@ -3971,6 +4101,10 @@ This is your link for group %@!</source>
<source>Messages sent</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages were deleted after you selected them." xml:space="preserve">
<source>Messages were deleted after you selected them.</source>
<note>alert message</note>
</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">
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
<note>No comment provided by engineer.</note>
@@ -4113,6 +4247,14 @@ This is your link for group %@!</source>
<target>Nové heslo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve">
<source>New SOCKS credentials will be used every time you start the app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve">
<source>New SOCKS credentials will be used for each server.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
@@ -4227,6 +4369,14 @@ This is your link for group %@!</source>
<source>No network connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record speech" xml:space="preserve">
<source>No permission to record speech</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record video" xml:space="preserve">
<source>No permission to record video</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>Nemáte oprávnění nahrávat hlasové zprávy</target>
@@ -4245,6 +4395,10 @@ This is your link for group %@!</source>
<source>Nothing selected</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Nothing to forward!" xml:space="preserve">
<source>Nothing to forward!</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Oznámení</target>
@@ -4276,7 +4430,7 @@ This is your link for group %@!</source>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Old database" xml:space="preserve">
<source>Old database</source>
@@ -4456,6 +4610,11 @@ Vyžaduje povolení sítě VPN.</target>
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other file errors:&#10;%@" xml:space="preserve">
<source>Other file errors:
%@</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>Počet PING</target>
@@ -4491,6 +4650,10 @@ Vyžaduje povolení sítě VPN.</target>
<target>Heslo nastaveno!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password" xml:space="preserve">
<source>Password</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password to show" xml:space="preserve">
<source>Password to show</source>
<target>Heslo k zobrazení</target>
@@ -4627,6 +4790,10 @@ Error: %@</source>
<target>Polské rozhraní</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Port" xml:space="preserve">
<source>Port</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve">
<source>Possibly, certificate fingerprint in server address is incorrect</source>
<target>Je možné, že otisk certifikátu v adrese serveru je nesprávný</target>
@@ -4705,14 +4872,6 @@ Error: %@</source>
<source>Profile images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source>
<target>Heslo profilu</target>
@@ -4808,6 +4967,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Proxied servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Proxy requires password" xml:space="preserve">
<source>Proxy requires password</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>Nabízená oznámení</target>
@@ -5014,6 +5177,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Odstranit</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove image" xml:space="preserve">
<source>Remove image</source>
<note>No comment provided by engineer.</note>
@@ -5181,6 +5348,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>SMP server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SOCKS proxy" xml:space="preserve">
<source>SOCKS proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Safely receive files" xml:space="preserve">
<source>Safely receive files</source>
<note>No comment provided by engineer.</note>
@@ -5192,12 +5363,13 @@ Enable in *Network &amp; servers* settings.</source>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<target>Uložit</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
<source>Save (and notify contacts)</source>
<target>Uložit (a informovat kontakty)</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Save and notify contact" xml:space="preserve">
<source>Save and notify contact</source>
@@ -5223,11 +5395,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Uložit archiv</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save auto-accept settings" xml:space="preserve">
<source>Save auto-accept settings</source>
<target>Uložit nastavení automatického přijímání</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save group profile" xml:space="preserve">
<source>Save group profile</source>
<target>Uložení profilu skupiny</target>
@@ -5263,16 +5430,15 @@ Enable in *Network &amp; servers* settings.</source>
<target>Uložit servery?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save settings?" xml:space="preserve">
<source>Save settings?</source>
<target>Uložit nastavení?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save welcome message?" xml:space="preserve">
<source>Save welcome message?</source>
<target>Uložit uvítací zprávu?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save your profile?" xml:space="preserve">
<source>Save your profile?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Saved" xml:space="preserve">
<source>Saved</source>
<note>No comment provided by engineer.</note>
@@ -5290,6 +5456,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Saved message</source>
<note>message info title</note>
</trans-unit>
<trans-unit id="Saving %lld messages" xml:space="preserve">
<source>Saving %lld messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scale" xml:space="preserve">
<source>Scale</source>
<note>No comment provided by engineer.</note>
@@ -5363,6 +5533,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Vybrat</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<note>No comment provided by engineer.</note>
@@ -5474,7 +5648,7 @@ Enable in *Network &amp; servers* settings.</source>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>Odesílatel zrušil přenos souboru.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Sender may have deleted the connection request." xml:space="preserve">
<source>Sender may have deleted the connection request.</source>
@@ -5566,6 +5740,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Sent via proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server address" xml:space="preserve">
<source>Server address</source>
<note>No comment provided by engineer.</note>
@@ -5675,6 +5853,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Nastavení</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Settings were changed." xml:space="preserve">
<source>Settings were changed.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Shape profile images" xml:space="preserve">
<source>Shape profile images</source>
<note>No comment provided by engineer.</note>
@@ -5708,6 +5890,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Sdílet odkaz</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<note>No comment provided by engineer.</note>
@@ -5834,6 +6020,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Jednorázová pozvánka SimpleX</target>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Zjednodušený inkognito režim</target>
@@ -5862,6 +6052,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Soft</source>
<note>blur media</note>
</trans-unit>
<trans-unit id="Some app settings were not migrated." xml:space="preserve">
<source>Some app settings were not migrated.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some file(s) were not exported:" xml:space="preserve">
<source>Some file(s) were not exported:</source>
<note>No comment provided by engineer.</note>
@@ -5990,6 +6184,14 @@ Enable in *Network &amp; servers* settings.</source>
<target>Podpořte SimpleX Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>Systém</target>
@@ -6024,6 +6226,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Vyfotit</target>
@@ -6207,6 +6413,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve">
<source>The uploaded database archive will be permanently removed from the servers.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Themes" xml:space="preserve">
<source>Themes</source>
<note>No comment provided by engineer.</note>
@@ -6323,6 +6533,14 @@ You will be prompted to complete authentication before this feature is enabled.<
Před zapnutím této funkce budete vyzváni k dokončení ověření.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve">
<source>To record speech please grant permission to use Microphone.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record video please grant permission to use Camera." xml:space="preserve">
<source>To record video please grant permission to use Camera.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve">
<source>To record voice message please grant permission to use Microphone.</source>
<target>Chcete-li nahrávat hlasové zprávy, udělte povolení k použití mikrofonu.</target>
@@ -6465,7 +6683,7 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target>
</trans-unit>
<trans-unit id="Unknown servers!" xml:space="preserve">
<source>Unknown servers!</source>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
@@ -6570,6 +6788,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<target>Použít hostitele .onion</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SOCKS proxy" xml:space="preserve">
<source>Use SOCKS proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
<source>Use SimpleX Chat servers?</source>
<target>Používat servery SimpleX Chat?</target>
@@ -6629,15 +6851,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<source>Use the app with one hand.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User profile" xml:space="preserve">
<source>User profile</source>
<target>Profil uživatele</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User selection" xml:space="preserve">
<source>User selection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
<source>Using SimpleX Chat servers.</source>
<target>Používat servery SimpleX Chat.</target>
@@ -6850,7 +7071,7 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
</trans-unit>
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
@@ -6874,11 +7095,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<source>XFTP server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You" xml:space="preserve">
<source>You</source>
<target>Vy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
<source>You **must not** use the same database on two devices.</source>
<note>No comment provided by engineer.</note>
@@ -7204,11 +7420,19 @@ Repeat connection request?</source>
<target>Vaše chat databáze není šifrována nastavte přístupovou frázi pro její šifrování.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat preferences" xml:space="preserve">
<source>Your chat preferences</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>Vaše chat profily</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (%@).</target>
@@ -7224,6 +7448,10 @@ Repeat connection request?</source>
<target>Vaše kontakty zůstanou připojeny.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve">
<source>Your credentials may be sent unencrypted.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve">
<source>Your current chat database will be DELETED and REPLACED with the imported one.</source>
<target>Vaše aktuální chat databáze bude ODSTRANĚNA a NAHRAZENA importovanou.</target>
@@ -7253,13 +7481,15 @@ Repeat connection request?</source>
<target>Váš profil **%@** bude sdílen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
<target>Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty.
Servery SimpleX nevidí váš profil.</target>
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
<target>Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. Servery SimpleX nevidí váš profil.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve">
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení.</target>
@@ -137,6 +137,11 @@
<target>%@ will sich mit Ihnen verbinden!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<target>%1$@, %2$@</target>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ und %lld Mitglieder</target>
@@ -157,11 +162,36 @@
<target>%d Tage</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve">
<source>%d file(s) are still being downloaded.</source>
<target>%d Datei(en) wird/werden immer noch heruntergeladen.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) failed to download." xml:space="preserve">
<source>%d file(s) failed to download.</source>
<target>Bei %d Datei(en) ist das Herunterladen fehlgeschlagen.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were deleted." xml:space="preserve">
<source>%d file(s) were deleted.</source>
<target>%d Datei(en) wurde(n) gelöscht.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve">
<source>%d file(s) were not downloaded.</source>
<target>%d Datei(en) wurde(n) nicht heruntergeladen.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d hours" xml:space="preserve">
<source>%d hours</source>
<target>%d Stunden</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d messages not forwarded" xml:space="preserve">
<source>%d messages not forwarded</source>
<target>%d Nachrichten wurden nicht weitergeleitet</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="%d min" xml:space="preserve">
<source>%d min</source>
<target>%d min</target>
@@ -723,7 +753,7 @@
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<target>Alle Profile</target>
<note>No comment provided by engineer.</note>
<note>profile dropdown</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source>
@@ -915,6 +945,11 @@
<target>App-Zugangscode wurde durch den Selbstzerstörungs-Zugangscode ersetzt.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App session" xml:space="preserve">
<source>App session</source>
<target>App-Sitzung</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App version" xml:space="preserve">
<source>App version</source>
<target>App Version</target>
@@ -1020,6 +1055,11 @@
<target>Bilder automatisch akzeptieren</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<target>Einstellungen automatisch akzeptieren</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
<source>Back</source>
<target>Zurück</target>
@@ -1045,11 +1085,21 @@
<target>Ungültiger Nachrichten-Hash</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source>
<target>Verbesserte Anrufe</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<target>Bessere Gruppen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source>
<target>Verbesserte Nachrichten-Datumsinformation</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>Verbesserungen bei Nachrichten</target>
@@ -1060,6 +1110,21 @@
<target>Kontrollieren Sie Ihr Netzwerk</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source>
<target>Verbesserte Benachrichtigungen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source>
<target>Verbesserte Sicherheit ✅</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source>
<target>Verbesserte Nutzer-Erfahrung</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Black" xml:space="preserve">
<source>Black</source>
<target>Schwarz</target>
@@ -1193,7 +1258,7 @@
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Abbrechen</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve">
<source>Cancel migration</source>
@@ -1213,7 +1278,7 @@
<trans-unit id="Cannot receive file" xml:space="preserve">
<source>Cannot receive file</source>
<target>Datei kann nicht empfangen werden</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
@@ -1341,6 +1406,16 @@
<target>Chat-Präferenzen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences were changed." xml:space="preserve">
<source>Chat preferences were changed.</source>
<target>Die Chat-Präferenzen wurden geändert.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Chat profile" xml:space="preserve">
<source>Chat profile</source>
<target>Benutzerprofil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat theme" xml:space="preserve">
<source>Chat theme</source>
<target>Chat-Design</target>
@@ -1393,22 +1468,22 @@
</trans-unit>
<trans-unit id="Clear" xml:space="preserve">
<source>Clear</source>
<target>Löschen</target>
<target>Entfernen</target>
<note>swipe action</note>
</trans-unit>
<trans-unit id="Clear conversation" xml:space="preserve">
<source>Clear conversation</source>
<target>Chatinhalte löschen</target>
<target>Chat-Inhalte entfernen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear conversation?" xml:space="preserve">
<source>Clear conversation?</source>
<target>Unterhaltung löschen?</target>
<target>Chat-Inhalte entfernen?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear private notes?" xml:space="preserve">
<source>Clear private notes?</source>
<target>Private Notizen löschen?</target>
<target>Private Notizen entfernen?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Clear verification" xml:space="preserve">
@@ -1722,7 +1797,7 @@ Das ist Ihr eigener Einmal-Link!</target>
</trans-unit>
<trans-unit id="Conversation deleted!" xml:space="preserve">
<source>Conversation deleted!</source>
<target>Unterhaltung gelöscht!</target>
<target>Chat-Inhalte entfernt!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Copy" xml:space="preserve">
@@ -1740,6 +1815,11 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Core Version: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<target>Abrundung Ecken</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Richtiger Name für %@?</target>
@@ -1865,6 +1945,11 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Zeit anpassen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source>
<target>Anpassbares Format des Nachrichtenfelds</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customize theme" xml:space="preserve">
<source>Customize theme</source>
<target>Design anpassen</target>
@@ -2159,6 +2244,11 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Alte Datenbank löschen?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve">
<source>Delete pending connection?</source>
<target>Ausstehende Verbindung löschen?</target>
@@ -2409,6 +2499,11 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Den Nachrichtenverlauf nicht an neue Mitglieder senden.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not use credentials with proxy." xml:space="preserve">
<source>Do not use credentials with proxy.</source>
<target>Verwenden Sie keine Anmeldeinformationen mit einem Proxy.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>Keine Adresse erstellt</target>
@@ -2432,7 +2527,8 @@ Das ist Ihr eigener Einmal-Link!</target>
<trans-unit id="Download" xml:space="preserve">
<source>Download</source>
<target>Herunterladen</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Download errors" xml:space="preserve">
<source>Download errors</source>
@@ -2449,6 +2545,11 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Datei herunterladen</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Download files" xml:space="preserve">
<source>Download files</source>
<target>Dateien herunterladen</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Downloaded" xml:space="preserve">
<source>Downloaded</source>
<target>Heruntergeladen</target>
@@ -2724,6 +2825,11 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Fehler beim Wechseln der Empfängeradresse</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<target>Fehler beim Wechseln des Verbindungs-Profils</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Fehler beim Ändern der Rolle</target>
@@ -2734,6 +2840,11 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Fehler beim Ändern der Einstellung</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<target>Fehler beim Wechseln zum Inkognito-Profil!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Fehler beim Verbinden mit dem Weiterleitungsserver %@. Bitte versuchen Sie es später erneut.</target>
@@ -2854,6 +2965,11 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Fehler beim Laden von %@ Servern</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error migrating settings" xml:space="preserve">
<source>Error migrating settings</source>
<target>Fehler beim Migrieren der Einstellungen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<target>Fehler beim Öffnen des Chats</target>
@@ -2862,7 +2978,7 @@ Das ist Ihr eigener Einmal-Link!</target>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Fehler beim Empfangen der Datei</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Error reconnecting server" xml:space="preserve">
<source>Error reconnecting server</source>
@@ -2954,10 +3070,15 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Fehler beim Beenden des Chats</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<target>Fehler beim Wechseln des Profils</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Fehler beim Umschalten des Profils!</target>
<note>No comment provided by engineer.</note>
<note>alertTitle</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
@@ -3002,8 +3123,7 @@ Das ist Ihr eigener Einmal-Link!</target>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>Fehler: %@</target>
<note>file error text
snd error text</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Error: URL is invalid" xml:space="preserve">
<source>Error: URL is invalid</source>
@@ -3090,6 +3210,13 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Datei-Fehler</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File errors:&#10;%@" xml:space="preserve">
<source>File errors:
%@</source>
<target>Datei-Fehler:
%@</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve">
<source>File not found - most likely file was deleted or cancelled.</source>
<target>Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen.</target>
@@ -3225,11 +3352,31 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Weiterleiten</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Forward %d message(s)?" xml:space="preserve">
<source>Forward %d message(s)?</source>
<target>%d Nachricht(en) weiterleiten?</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Forward and save messages" xml:space="preserve">
<source>Forward and save messages</source>
<target>Nachrichten weiterleiten und speichern</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forward messages" xml:space="preserve">
<source>Forward messages</source>
<target>Nachrichten weiterleiten</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Forward messages without files?" xml:space="preserve">
<source>Forward messages without files?</source>
<target>Nachrichten ohne Dateien weiterleiten?</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="Forwarded" xml:space="preserve">
<source>Forwarded</source>
<target>Weitergeleitet</target>
@@ -3240,6 +3387,11 @@ Das ist Ihr eigener Einmal-Link!</target>
<target>Weitergeleitet aus</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding %lld messages" xml:space="preserve">
<source>Forwarding %lld messages</source>
<target>%lld Nachricht(en) wird/werden weitergeleitet</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve">
<source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source>
<target>Weiterleitungsserver %@ konnte sich nicht mit dem Zielserver %@ verbinden. Bitte versuchen Sie es später erneut.</target>
@@ -3289,11 +3441,6 @@ Fehler: %2$@</target>
<target>Vollständiger Name (optional)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Full name:" xml:space="preserve">
<source>Full name:</source>
<target>Vollständiger Name:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<target>Vollständig dezentralisiert nur für Mitglieder sichtbar.</target>
@@ -3451,7 +3598,7 @@ Fehler: %2$@</target>
</trans-unit>
<trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve">
<source>Group will be deleted for you - this cannot be undone!</source>
<target>Die Gruppe wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!</target>
<target>Die Gruppe wird nur bei Ihnen gelöscht. Dies kann nicht rückgängig gemacht werden!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Help" xml:space="preserve">
@@ -3539,6 +3686,11 @@ Fehler: %2$@</target>
<target>ICE-Server (einer pro Zeile)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="IP address" xml:space="preserve">
<source>IP address</source>
<target>IP-Adresse</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
<target>Falls Sie sich nicht persönlich treffen können, zeigen Sie den QR-Code in einem Videoanruf oder teilen Sie den Link.</target>
@@ -3614,6 +3766,13 @@ Fehler: %2$@</target>
<target>Archiv wird importiert</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage.
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>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<target>Verbesserte Zustellung von Nachrichten</target>
@@ -3906,7 +4065,7 @@ Das ist Ihr Link für die Gruppe %@!</target>
</trans-unit>
<trans-unit id="Keep conversation" xml:space="preserve">
<source>Keep conversation</source>
<target>Unterhaltung behalten</target>
<target>Chat-Inhalte beibehalten</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Keep the app open to use it from desktop" xml:space="preserve">
@@ -4184,6 +4343,11 @@ Das ist Ihr Link für die Gruppe %@!</target>
<target>Nachrichten-Server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<target>Nachrichten-Form</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Die Nachrichtenquelle bleibt privat.</target>
@@ -4234,6 +4398,11 @@ Das ist Ihr Link für die Gruppe %@!</target>
<target>Gesendete Nachrichten</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages were deleted after you selected them." xml:space="preserve">
<source>Messages were deleted after you selected them.</source>
<target>Die Nachrichten wurden gelöscht, nachdem Sie sie ausgewählt hatten.</target>
<note>alert message</note>
</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">
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
<target>Nachrichten, Dateien und Anrufe sind durch **Ende-zu-Ende-Verschlüsselung** mit Perfect Forward Secrecy, Ablehnung und Einbruchs-Wiederherstellung geschützt.</target>
@@ -4389,6 +4558,16 @@ Das ist Ihr Link für die Gruppe %@!</target>
<target>Neuer Zugangscode</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve">
<source>New SOCKS credentials will be used every time you start the app.</source>
<target>Jedes Mal wenn Sie die App starten, werden neue SOCKS-Anmeldeinformationen genutzt</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve">
<source>New SOCKS credentials will be used for each server.</source>
<target>Für jeden Server werden neue SOCKS-Anmeldeinformationen genutzt</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<target>Neuer Chat</target>
@@ -4509,6 +4688,16 @@ Das ist Ihr Link für die Gruppe %@!</target>
<target>Keine Netzwerkverbindung</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record speech" xml:space="preserve">
<source>No permission to record speech</source>
<target>Keine Genehmigung für Sprach-Aufnahmen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record video" xml:space="preserve">
<source>No permission to record video</source>
<target>Keine Genehmigung für Video-Aufnahmen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>Keine Berechtigung für das Aufnehmen von Sprachnachrichten</target>
@@ -4529,6 +4718,11 @@ Das ist Ihr Link für die Gruppe %@!</target>
<target>Nichts ausgewählt</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Nothing to forward!" xml:space="preserve">
<source>Nothing to forward!</source>
<target>Es gibt nichts zum Weiterleiten!</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Benachrichtigungen</target>
@@ -4561,7 +4755,7 @@ Das ist Ihr Link für die Gruppe %@!</target>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Old database" xml:space="preserve">
<source>Old database</source>
@@ -4604,7 +4798,7 @@ Dies erfordert die Aktivierung eines VPNs.</target>
</trans-unit>
<trans-unit id="Only delete conversation" xml:space="preserve">
<source>Only delete conversation</source>
<target>Nur die Unterhaltung löschen</target>
<target>Nur die Chat-Inhalte löschen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
@@ -4752,6 +4946,13 @@ Dies erfordert die Aktivierung eines VPNs.</target>
<target>Andere %@ Server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other file errors:&#10;%@" xml:space="preserve">
<source>Other file errors:
%@</source>
<target>Andere(r) Datei-Fehler:
%@</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>PING-Zähler</target>
@@ -4787,6 +4988,11 @@ Dies erfordert die Aktivierung eines VPNs.</target>
<target>Zugangscode eingestellt!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password" xml:space="preserve">
<source>Password</source>
<target>Passwort</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password to show" xml:space="preserve">
<source>Password to show</source>
<target>Passwort anzeigen</target>
@@ -4936,6 +5142,11 @@ Fehler: %@</target>
<target>Polnische Bedienoberfläche</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Port" xml:space="preserve">
<source>Port</source>
<target>Port</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve">
<source>Possibly, certificate fingerprint in server address is incorrect</source>
<target>Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig</target>
@@ -5021,16 +5232,6 @@ Fehler: %@</target>
<target>Profil-Bilder</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<target>Profilname</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<target>Profilname:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source>
<target>Passwort für Profil</target>
@@ -5133,6 +5334,11 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Proxy-Server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Proxy requires password" xml:space="preserve">
<source>Proxy requires password</source>
<target>Der Proxy benötigt ein Passwort</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>Push-Benachrichtigungen</target>
@@ -5155,7 +5361,7 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
</trans-unit>
<trans-unit id="Reachable chat toolbar" xml:space="preserve">
<source>Reachable chat toolbar</source>
<target>Erreichbare Chat-Symbolleiste</target>
<target>Chat-Symbolleiste unten</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="React…" xml:space="preserve">
@@ -5354,6 +5560,11 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Entfernen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
<target>Archiv entfernen?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove image" xml:space="preserve">
<source>Remove image</source>
<target>Bild entfernen</target>
@@ -5534,6 +5745,11 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>SMP-Server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SOCKS proxy" xml:space="preserve">
<source>SOCKS proxy</source>
<target>SOCKS-Proxy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Safely receive files" xml:space="preserve">
<source>Safely receive files</source>
<target>Dateien sicher empfangen</target>
@@ -5547,12 +5763,13 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<target>Speichern</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
<source>Save (and notify contacts)</source>
<target>Speichern (und Kontakte benachrichtigen)</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Save and notify contact" xml:space="preserve">
<source>Save and notify contact</source>
@@ -5579,11 +5796,6 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Archiv speichern</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save auto-accept settings" xml:space="preserve">
<source>Save auto-accept settings</source>
<target>Einstellungen von "Automatisch akzeptieren" speichern</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save group profile" xml:space="preserve">
<source>Save group profile</source>
<target>Gruppenprofil speichern</target>
@@ -5619,16 +5831,16 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Alle Server speichern?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save settings?" xml:space="preserve">
<source>Save settings?</source>
<target>Einstellungen speichern?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save welcome message?" xml:space="preserve">
<source>Save welcome message?</source>
<target>Begrüßungsmeldung speichern?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save your profile?" xml:space="preserve">
<source>Save your profile?</source>
<target>Ihr Profil speichern?</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Saved" xml:space="preserve">
<source>Saved</source>
<target>Abgespeichert</target>
@@ -5649,6 +5861,11 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Gespeicherte Nachricht</target>
<note>message info title</note>
</trans-unit>
<trans-unit id="Saving %lld messages" xml:space="preserve">
<source>Saving %lld messages</source>
<target>Es wird/werden %lld Nachricht(en) gesichert</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scale" xml:space="preserve">
<source>Scale</source>
<target>Skalieren</target>
@@ -5696,7 +5913,7 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<target>Suchen oder fügen Sie den SimpleX-Link ein</target>
<target>Suchen oder SimpleX-Link einfügen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secondary" xml:space="preserve">
@@ -5729,6 +5946,11 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Auswählen</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<target>Chat-Profil auswählen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>%lld ausgewählt</target>
@@ -5847,7 +6069,7 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>Der Absender hat die Dateiübertragung abgebrochen.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Sender may have deleted the connection request." xml:space="preserve">
<source>Sender may have deleted the connection request.</source>
@@ -5944,6 +6166,11 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Über einen Proxy gesendet</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<target>Server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server address" xml:space="preserve">
<source>Server address</source>
<target>Server-Adresse</target>
@@ -6064,6 +6291,11 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Einstellungen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Settings were changed." xml:space="preserve">
<source>Settings were changed.</source>
<target>Die Einstellungen wurden geändert.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Shape profile images" xml:space="preserve">
<source>Shape profile images</source>
<target>Form der Profil-Bilder</target>
@@ -6099,6 +6331,11 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Link teilen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<target>Profil teilen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Teilen Sie diesen Einmal-Einladungslink</target>
@@ -6234,6 +6471,11 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>SimpleX-Einmal-Einladung</target>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Vereinfachter Inkognito-Modus</target>
@@ -6264,6 +6506,11 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Weich</target>
<note>blur media</note>
</trans-unit>
<trans-unit id="Some app settings were not migrated." xml:space="preserve">
<source>Some app settings were not migrated.</source>
<target>Einige App-Einstellungen wurden nicht migriert.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some file(s) were not exported:" xml:space="preserve">
<source>Some file(s) were not exported:</source>
<target>Einzelne Datei(en) wurde(n) nicht exportiert:</target>
@@ -6404,6 +6651,16 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>Unterstützung von SimpleX Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>System</target>
@@ -6439,6 +6696,11 @@ Aktivieren Sie es in den *Netzwerk &amp; Server* Einstellungen.</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<target>Sprechblasen-Format</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Machen Sie ein Foto</target>
@@ -6588,7 +6850,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro
</trans-unit>
<trans-unit id="The messages will be deleted for all members." xml:space="preserve">
<source>The messages will be deleted for all members.</source>
<target>Die Nachrichten werden für alle Mitglieder gelöscht werden.</target>
<target>Die Nachrichten werden für alle Gruppenmitglieder gelöscht.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The messages will be marked as moderated for all members." xml:space="preserve">
@@ -6631,6 +6893,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro
<target>Der von Ihnen eingefügte Text ist kein SimpleX-Link.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve">
<source>The uploaded database archive will be permanently removed from the servers.</source>
<target>Das hochgeladene Datenbank-Archiv wird dauerhaft von den Servern entfernt.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Themes" xml:space="preserve">
<source>Themes</source>
<target>Design</target>
@@ -6758,6 +7025,16 @@ You will be prompted to complete authentication before this feature is enabled.<
Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funktion aktiviert wird.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve">
<source>To record speech please grant permission to use Microphone.</source>
<target>Bitte erteilen Sie für Sprach-Aufnahmen die Genehmigung das Mikrofon zu nutzen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record video please grant permission to use Camera." xml:space="preserve">
<source>To record video please grant permission to use Camera.</source>
<target>Bitte erteilen Sie für Video-Aufnahmen die Genehmigung die Kamera zu nutzen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve">
<source>To record voice message please grant permission to use Microphone.</source>
<target>Bitte erlauben Sie die Nutzung des Mikrofons, um Sprachnachrichten aufnehmen zu können.</target>
@@ -6911,7 +7188,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
<trans-unit id="Unknown servers!" xml:space="preserve">
<source>Unknown servers!</source>
<target>Unbekannte Server!</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
@@ -7025,6 +7302,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
<target>Verwende .onion-Hosts</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SOCKS proxy" xml:space="preserve">
<source>Use SOCKS proxy</source>
<target>SOCKS-Proxy nutzen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
<source>Use SimpleX Chat servers?</source>
<target>Verwenden Sie SimpleX-Chat-Server?</target>
@@ -7087,12 +7369,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
</trans-unit>
<trans-unit id="Use the app with one hand." xml:space="preserve">
<source>Use the app with one hand.</source>
<target>Die App mit einer Hand nutzen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User profile" xml:space="preserve">
<source>User profile</source>
<target>Benutzerprofil</target>
<target>Die App mit einer Hand bedienen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User selection" xml:space="preserve">
@@ -7100,6 +7377,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
<target>Benutzer-Auswahl</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<target>Benutzername</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
<source>Using SimpleX Chat servers.</source>
<target>Verwendung von SimpleX-Chat-Servern.</target>
@@ -7333,7 +7615,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
<target>Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für diese XFTP-Relais sichtbar sein: %@.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
@@ -7360,11 +7642,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
<target>XFTP-Server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You" xml:space="preserve">
<source>You</source>
<target>Profil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
<source>You **must not** use the same database on two devices.</source>
<target>Sie dürfen die selbe Datenbank **nicht** auf zwei Geräten nutzen.</target>
@@ -7519,7 +7796,7 @@ Verbindungsanfrage wiederholen?</target>
</trans-unit>
<trans-unit id="You can still view conversation with %@ in the list of chats." xml:space="preserve">
<source>You can still view conversation with %@ in the list of chats.</source>
<target>Sie können in der Chatliste weiterhin die Unterhaltung mit %@ einsehen.</target>
<target>Sie können in der Chat-Liste weiterhin die Unterhaltung mit %@ einsehen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve">
@@ -7714,11 +7991,21 @@ Verbindungsanfrage wiederholen?</target>
<target>Ihre Chat-Datenbank ist nicht verschlüsselt. Bitte legen Sie ein Passwort fest, um sie zu schützen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat preferences" xml:space="preserve">
<source>Your chat preferences</source>
<target>Ihre Chat-Präferenzen</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>Ihre Chat-Profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<target>Ihre Verbindung wurde auf %@ verschoben. Während Sie auf das Profil weitergeleitet wurden trat aber ein unerwarteter Fehler auf.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%@).</target>
@@ -7734,6 +8021,11 @@ Verbindungsanfrage wiederholen?</target>
<target>Ihre Kontakte bleiben weiterhin verbunden.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve">
<source>Your credentials may be sent unencrypted.</source>
<target>Ihre Anmeldeinformationen können unverschlüsselt versendet werden.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve">
<source>Your current chat database will be DELETED and REPLACED with the imported one.</source>
<target>Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die Importierte ERSETZT.</target>
@@ -7764,13 +8056,16 @@ Verbindungsanfrage wiederholen?</target>
<target>Ihr Profil **%@** wird geteilt.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
<target>Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.
SimpleX-Server können Ihr Profil nicht einsehen.</target>
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
<target>Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. SimpleX-Server können Ihr Profil nicht einsehen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve">
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
<target>Ihr Profil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an alle Ihre Kontakte gesendet.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert.</target>
@@ -8208,7 +8503,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="expired" xml:space="preserve">
<source>expired</source>
<target>abgelaufen</target>
<target>Abgelaufen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="forwarded" xml:space="preserve">
@@ -8440,7 +8735,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit>
<trans-unit id="other" xml:space="preserve">
<source>other</source>
<target>andere</target>
<target>Andere</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="other errors" xml:space="preserve">
@@ -4200,7 +4200,7 @@ SimpleX servers cannot see your profile.</source>
</trans-unit>
<trans-unit id="## In reply to" xml:space="preserve" approved="no">
<source>## In reply to</source>
<target state="translated">## Ως απαντηση σε</target>
<target state="translated">## Ως απάντηση σε</target>
<note>copied message info</note>
</trans-unit>
<trans-unit id="%@ (current)" xml:space="preserve" approved="no">
@@ -137,6 +137,11 @@
<target>%@ wants to connect!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<target>%1$@, %2$@</target>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ and %lld members</target>
@@ -157,11 +162,36 @@
<target>%d days</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve">
<source>%d file(s) are still being downloaded.</source>
<target>%d file(s) are still being downloaded.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) failed to download." xml:space="preserve">
<source>%d file(s) failed to download.</source>
<target>%d file(s) failed to download.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were deleted." xml:space="preserve">
<source>%d file(s) were deleted.</source>
<target>%d file(s) were deleted.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve">
<source>%d file(s) were not downloaded.</source>
<target>%d file(s) were not downloaded.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d hours" xml:space="preserve">
<source>%d hours</source>
<target>%d hours</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d messages not forwarded" xml:space="preserve">
<source>%d messages not forwarded</source>
<target>%d messages not forwarded</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="%d min" xml:space="preserve">
<source>%d min</source>
<target>%d min</target>
@@ -723,7 +753,7 @@
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<target>All profiles</target>
<note>No comment provided by engineer.</note>
<note>profile dropdown</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source>
@@ -915,6 +945,11 @@
<target>App passcode is replaced with self-destruct passcode.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App session" xml:space="preserve">
<source>App session</source>
<target>App session</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App version" xml:space="preserve">
<source>App version</source>
<target>App version</target>
@@ -1020,6 +1055,11 @@
<target>Auto-accept images</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<target>Auto-accept settings</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
<source>Back</source>
<target>Back</target>
@@ -1045,11 +1085,21 @@
<target>Bad message hash</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source>
<target>Better calls</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<target>Better groups</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source>
<target>Better message dates.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>Better messages</target>
@@ -1060,6 +1110,21 @@
<target>Better networking</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source>
<target>Better notifications</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source>
<target>Better security ✅</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source>
<target>Better user experience</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Black" xml:space="preserve">
<source>Black</source>
<target>Black</target>
@@ -1193,7 +1258,7 @@
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Cancel</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve">
<source>Cancel migration</source>
@@ -1213,7 +1278,7 @@
<trans-unit id="Cannot receive file" xml:space="preserve">
<source>Cannot receive file</source>
<target>Cannot receive file</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
@@ -1341,6 +1406,16 @@
<target>Chat preferences</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences were changed." xml:space="preserve">
<source>Chat preferences were changed.</source>
<target>Chat preferences were changed.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Chat profile" xml:space="preserve">
<source>Chat profile</source>
<target>Chat profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat theme" xml:space="preserve">
<source>Chat theme</source>
<target>Chat theme</target>
@@ -1740,6 +1815,11 @@ This is your own one-time link!</target>
<target>Core version: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<target>Corner</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Correct name to %@?</target>
@@ -1865,6 +1945,11 @@ This is your own one-time link!</target>
<target>Custom time</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source>
<target>Customizable message shape.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customize theme" xml:space="preserve">
<source>Customize theme</source>
<target>Customize theme</target>
@@ -2159,6 +2244,11 @@ This is your own one-time link!</target>
<target>Delete old database?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source>
<target>Delete or moderate up to 200 messages.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve">
<source>Delete pending connection?</source>
<target>Delete pending connection?</target>
@@ -2409,6 +2499,11 @@ This is your own one-time link!</target>
<target>Do not send history to new members.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not use credentials with proxy." xml:space="preserve">
<source>Do not use credentials with proxy.</source>
<target>Do not use credentials with proxy.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>Don't create address</target>
@@ -2432,7 +2527,8 @@ This is your own one-time link!</target>
<trans-unit id="Download" xml:space="preserve">
<source>Download</source>
<target>Download</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Download errors" xml:space="preserve">
<source>Download errors</source>
@@ -2449,6 +2545,11 @@ This is your own one-time link!</target>
<target>Download file</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Download files" xml:space="preserve">
<source>Download files</source>
<target>Download files</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Downloaded" xml:space="preserve">
<source>Downloaded</source>
<target>Downloaded</target>
@@ -2724,6 +2825,11 @@ This is your own one-time link!</target>
<target>Error changing address</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<target>Error changing connection profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Error changing role</target>
@@ -2734,6 +2840,11 @@ This is your own one-time link!</target>
<target>Error changing setting</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<target>Error changing to incognito!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Error connecting to forwarding server %@. Please try later.</target>
@@ -2854,6 +2965,11 @@ This is your own one-time link!</target>
<target>Error loading %@ servers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error migrating settings" xml:space="preserve">
<source>Error migrating settings</source>
<target>Error migrating settings</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<target>Error opening chat</target>
@@ -2862,7 +2978,7 @@ This is your own one-time link!</target>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Error receiving file</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Error reconnecting server" xml:space="preserve">
<source>Error reconnecting server</source>
@@ -2954,10 +3070,15 @@ This is your own one-time link!</target>
<target>Error stopping chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<target>Error switching profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Error switching profile!</target>
<note>No comment provided by engineer.</note>
<note>alertTitle</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
@@ -3002,8 +3123,7 @@ This is your own one-time link!</target>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>Error: %@</target>
<note>file error text
snd error text</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Error: URL is invalid" xml:space="preserve">
<source>Error: URL is invalid</source>
@@ -3090,6 +3210,13 @@ This is your own one-time link!</target>
<target>File error</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File errors:&#10;%@" xml:space="preserve">
<source>File errors:
%@</source>
<target>File errors:
%@</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve">
<source>File not found - most likely file was deleted or cancelled.</source>
<target>File not found - most likely file was deleted or cancelled.</target>
@@ -3225,11 +3352,31 @@ This is your own one-time link!</target>
<target>Forward</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Forward %d message(s)?" xml:space="preserve">
<source>Forward %d message(s)?</source>
<target>Forward %d message(s)?</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Forward and save messages" xml:space="preserve">
<source>Forward and save messages</source>
<target>Forward and save messages</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forward messages" xml:space="preserve">
<source>Forward messages</source>
<target>Forward messages</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Forward messages without files?" xml:space="preserve">
<source>Forward messages without files?</source>
<target>Forward messages without files?</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source>
<target>Forward up to 20 messages at once.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarded" xml:space="preserve">
<source>Forwarded</source>
<target>Forwarded</target>
@@ -3240,6 +3387,11 @@ This is your own one-time link!</target>
<target>Forwarded from</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding %lld messages" xml:space="preserve">
<source>Forwarding %lld messages</source>
<target>Forwarding %lld messages</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve">
<source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source>
<target>Forwarding server %@ failed to connect to destination server %@. Please try later.</target>
@@ -3289,11 +3441,6 @@ Error: %2$@</target>
<target>Full name (optional)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Full name:" xml:space="preserve">
<source>Full name:</source>
<target>Full name:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<target>Fully decentralized visible only to members.</target>
@@ -3539,6 +3686,11 @@ Error: %2$@</target>
<target>ICE servers (one per line)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="IP address" xml:space="preserve">
<source>IP address</source>
<target>IP address</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
<target>If you can't meet in person, show QR code in a video call, or share the link.</target>
@@ -3614,6 +3766,13 @@ Error: %2$@</target>
<target>Importing archive</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source>
<target>Improved delivery, reduced traffic usage.
More improvements are coming soon!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<target>Improved message delivery</target>
@@ -4184,6 +4343,11 @@ This is your link for group %@!</target>
<target>Message servers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<target>Message shape</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Message source remains private.</target>
@@ -4234,6 +4398,11 @@ This is your link for group %@!</target>
<target>Messages sent</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages were deleted after you selected them." xml:space="preserve">
<source>Messages were deleted after you selected them.</source>
<target>Messages were deleted after you selected them.</target>
<note>alert message</note>
</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">
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
<target>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</target>
@@ -4389,6 +4558,16 @@ This is your link for group %@!</target>
<target>New Passcode</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve">
<source>New SOCKS credentials will be used every time you start the app.</source>
<target>New SOCKS credentials will be used every time you start the app.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve">
<source>New SOCKS credentials will be used for each server.</source>
<target>New SOCKS credentials will be used for each server.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<target>New chat</target>
@@ -4509,6 +4688,16 @@ This is your link for group %@!</target>
<target>No network connection</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record speech" xml:space="preserve">
<source>No permission to record speech</source>
<target>No permission to record speech</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record video" xml:space="preserve">
<source>No permission to record video</source>
<target>No permission to record video</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>No permission to record voice message</target>
@@ -4529,6 +4718,11 @@ This is your link for group %@!</target>
<target>Nothing selected</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Nothing to forward!" xml:space="preserve">
<source>Nothing to forward!</source>
<target>Nothing to forward!</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Notifications</target>
@@ -4561,7 +4755,7 @@ This is your link for group %@!</target>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Old database" xml:space="preserve">
<source>Old database</source>
@@ -4752,6 +4946,13 @@ Requires compatible VPN.</target>
<target>Other %@ servers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other file errors:&#10;%@" xml:space="preserve">
<source>Other file errors:
%@</source>
<target>Other file errors:
%@</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>PING count</target>
@@ -4787,6 +4988,11 @@ Requires compatible VPN.</target>
<target>Passcode set!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password" xml:space="preserve">
<source>Password</source>
<target>Password</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password to show" xml:space="preserve">
<source>Password to show</source>
<target>Password to show</target>
@@ -4936,6 +5142,11 @@ Error: %@</target>
<target>Polish interface</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Port" xml:space="preserve">
<source>Port</source>
<target>Port</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve">
<source>Possibly, certificate fingerprint in server address is incorrect</source>
<target>Possibly, certificate fingerprint in server address is incorrect</target>
@@ -5021,16 +5232,6 @@ Error: %@</target>
<target>Profile images</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<target>Profile name</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<target>Profile name:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source>
<target>Profile password</target>
@@ -5133,6 +5334,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>Proxied servers</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Proxy requires password" xml:space="preserve">
<source>Proxy requires password</source>
<target>Proxy requires password</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>Push notifications</target>
@@ -5354,6 +5560,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>Remove</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
<target>Remove archive?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove image" xml:space="preserve">
<source>Remove image</source>
<target>Remove image</target>
@@ -5534,6 +5745,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>SMP server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SOCKS proxy" xml:space="preserve">
<source>SOCKS proxy</source>
<target>SOCKS proxy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Safely receive files" xml:space="preserve">
<source>Safely receive files</source>
<target>Safely receive files</target>
@@ -5547,12 +5763,13 @@ Enable in *Network &amp; servers* settings.</target>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<target>Save</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
<source>Save (and notify contacts)</source>
<target>Save (and notify contacts)</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Save and notify contact" xml:space="preserve">
<source>Save and notify contact</source>
@@ -5579,11 +5796,6 @@ Enable in *Network &amp; servers* settings.</target>
<target>Save archive</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save auto-accept settings" xml:space="preserve">
<source>Save auto-accept settings</source>
<target>Save auto-accept settings</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save group profile" xml:space="preserve">
<source>Save group profile</source>
<target>Save group profile</target>
@@ -5619,16 +5831,16 @@ Enable in *Network &amp; servers* settings.</target>
<target>Save servers?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save settings?" xml:space="preserve">
<source>Save settings?</source>
<target>Save settings?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save welcome message?" xml:space="preserve">
<source>Save welcome message?</source>
<target>Save welcome message?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save your profile?" xml:space="preserve">
<source>Save your profile?</source>
<target>Save your profile?</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Saved" xml:space="preserve">
<source>Saved</source>
<target>Saved</target>
@@ -5649,6 +5861,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>Saved message</target>
<note>message info title</note>
</trans-unit>
<trans-unit id="Saving %lld messages" xml:space="preserve">
<source>Saving %lld messages</source>
<target>Saving %lld messages</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scale" xml:space="preserve">
<source>Scale</source>
<target>Scale</target>
@@ -5729,6 +5946,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>Select</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<target>Select chat profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>Selected %lld</target>
@@ -5847,7 +6069,7 @@ Enable in *Network &amp; servers* settings.</target>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>Sender cancelled file transfer.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Sender may have deleted the connection request." xml:space="preserve">
<source>Sender may have deleted the connection request.</source>
@@ -5944,6 +6166,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>Sent via proxy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<target>Server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server address" xml:space="preserve">
<source>Server address</source>
<target>Server address</target>
@@ -6064,6 +6291,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>Settings</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Settings were changed." xml:space="preserve">
<source>Settings were changed.</source>
<target>Settings were changed.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Shape profile images" xml:space="preserve">
<source>Shape profile images</source>
<target>Shape profile images</target>
@@ -6099,6 +6331,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>Share link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<target>Share profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Share this 1-time invite link</target>
@@ -6234,6 +6471,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>SimpleX one-time invitation</target>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<target>SimpleX protocols reviewed by Trail of Bits.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Simplified incognito mode</target>
@@ -6264,6 +6506,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>Soft</target>
<note>blur media</note>
</trans-unit>
<trans-unit id="Some app settings were not migrated." xml:space="preserve">
<source>Some app settings were not migrated.</source>
<target>Some app settings were not migrated.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some file(s) were not exported:" xml:space="preserve">
<source>Some file(s) were not exported:</source>
<target>Some file(s) were not exported:</target>
@@ -6404,6 +6651,16 @@ Enable in *Network &amp; servers* settings.</target>
<target>Support SimpleX Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source>
<target>Switch audio and video during the call.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source>
<target>Switch chat profile for 1-time invitations.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>System</target>
@@ -6439,6 +6696,11 @@ Enable in *Network &amp; servers* settings.</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<target>Tail</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Take picture</target>
@@ -6631,6 +6893,11 @@ It can happen because of some bug or when the connection is compromised.</target
<target>The text you pasted is not a SimpleX link.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve">
<source>The uploaded database archive will be permanently removed from the servers.</source>
<target>The uploaded database archive will be permanently removed from the servers.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Themes" xml:space="preserve">
<source>Themes</source>
<target>Themes</target>
@@ -6758,6 +7025,16 @@ You will be prompted to complete authentication before this feature is enabled.<
You will be prompted to complete authentication before this feature is enabled.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve">
<source>To record speech please grant permission to use Microphone.</source>
<target>To record speech please grant permission to use Microphone.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record video please grant permission to use Camera." xml:space="preserve">
<source>To record video please grant permission to use Camera.</source>
<target>To record video please grant permission to use Camera.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve">
<source>To record voice message please grant permission to use Microphone.</source>
<target>To record voice message please grant permission to use Microphone.</target>
@@ -6911,7 +7188,7 @@ You will be prompted to complete authentication before this feature is enabled.<
<trans-unit id="Unknown servers!" xml:space="preserve">
<source>Unknown servers!</source>
<target>Unknown servers!</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
@@ -7025,6 +7302,11 @@ To connect, please ask your contact to create another connection link and check
<target>Use .onion hosts</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SOCKS proxy" xml:space="preserve">
<source>Use SOCKS proxy</source>
<target>Use SOCKS proxy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
<source>Use SimpleX Chat servers?</source>
<target>Use SimpleX Chat servers?</target>
@@ -7090,16 +7372,16 @@ To connect, please ask your contact to create another connection link and check
<target>Use the app with one hand.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User profile" xml:space="preserve">
<source>User profile</source>
<target>User profile</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User selection" xml:space="preserve">
<source>User selection</source>
<target>User selection</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<target>Username</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
<source>Using SimpleX Chat servers.</source>
<target>Using SimpleX Chat servers.</target>
@@ -7333,7 +7615,7 @@ To connect, please ask your contact to create another connection link and check
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
<target>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
@@ -7360,11 +7642,6 @@ To connect, please ask your contact to create another connection link and check
<target>XFTP server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You" xml:space="preserve">
<source>You</source>
<target>You</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
<source>You **must not** use the same database on two devices.</source>
<target>You **must not** use the same database on two devices.</target>
@@ -7714,11 +7991,21 @@ Repeat connection request?</target>
<target>Your chat database is not encrypted - set passphrase to encrypt it.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat preferences" xml:space="preserve">
<source>Your chat preferences</source>
<target>Your chat preferences</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>Your chat profiles</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<target>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Your contact sent a file that is larger than currently supported maximum size (%@).</target>
@@ -7734,6 +8021,11 @@ Repeat connection request?</target>
<target>Your contacts will remain connected.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve">
<source>Your credentials may be sent unencrypted.</source>
<target>Your credentials may be sent unencrypted.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve">
<source>Your current chat database will be DELETED and REPLACED with the imported one.</source>
<target>Your current chat database will be DELETED and REPLACED with the imported one.</target>
@@ -7764,13 +8056,16 @@ Repeat connection request?</target>
<target>Your profile **%@** will be shared.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
<target>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</target>
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
<target>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve">
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
<target>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>Your profile, contacts and delivered messages are stored on your device.</target>
@@ -137,6 +137,11 @@
<target>¡ %@ quiere contactar!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<target>%1$@, %2$@</target>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ y %lld miembro(s) más</target>
@@ -157,11 +162,36 @@
<target>%d días</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve">
<source>%d file(s) are still being downloaded.</source>
<target>%d archivo(s) se está(n) descargando todavía.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) failed to download." xml:space="preserve">
<source>%d file(s) failed to download.</source>
<target>La descarga ha fallado para %d archivo(s).</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were deleted." xml:space="preserve">
<source>%d file(s) were deleted.</source>
<target>%d archivo(s) ha(n) sido eliminado(s).</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve">
<source>%d file(s) were not downloaded.</source>
<target>%d archivo(s) no se ha(n) descargado.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d hours" xml:space="preserve">
<source>%d hours</source>
<target>%d horas</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d messages not forwarded" xml:space="preserve">
<source>%d messages not forwarded</source>
<target>%d mensajes no enviados</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="%d min" xml:space="preserve">
<source>%d min</source>
<target>%d minutos</target>
@@ -507,7 +537,7 @@
</trans-unit>
<trans-unit id="A new random profile will be shared." xml:space="preserve">
<source>A new random profile will be shared.</source>
<target>Se compartirá un perfil nuevo aleatorio.</target>
<target>Compartirás un perfil nuevo aleatorio.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve">
@@ -723,7 +753,7 @@
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<target>Todos los perfiles</target>
<note>No comment provided by engineer.</note>
<note>profile dropdown</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source>
@@ -802,7 +832,7 @@
</trans-unit>
<trans-unit id="Allow to send SimpleX links." xml:space="preserve">
<source>Allow to send SimpleX links.</source>
<target>Permitir enviar enlaces SimpleX.</target>
<target>Se permite enviar enlaces SimpleX.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve">
@@ -915,6 +945,11 @@
<target>El código de acceso será reemplazado por código de autodestrucción.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App session" xml:space="preserve">
<source>App session</source>
<target>Sesión de aplicación</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App version" xml:space="preserve">
<source>App version</source>
<target>Versión de la aplicación</target>
@@ -1020,6 +1055,11 @@
<target>Aceptar imágenes automáticamente</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<target>Auto aceptar configuración</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
<source>Back</source>
<target>Volver</target>
@@ -1045,11 +1085,21 @@
<target>Hash de mensaje incorrecto</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source>
<target>Llamadas mejoradas</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<target>Grupos mejorados</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source>
<target>Sistema de fechas mejorado.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>Mensajes mejorados</target>
@@ -1060,6 +1110,21 @@
<target>Uso de red mejorado</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source>
<target>Notificaciones mejoradas</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source>
<target>Seguridad mejorada ✅</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source>
<target>Experiencia de usuario mejorada</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Black" xml:space="preserve">
<source>Black</source>
<target>Negro</target>
@@ -1193,7 +1258,7 @@
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Cancelar</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve">
<source>Cancel migration</source>
@@ -1213,7 +1278,7 @@
<trans-unit id="Cannot receive file" xml:space="preserve">
<source>Cannot receive file</source>
<target>No se puede recibir el archivo</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
@@ -1341,6 +1406,16 @@
<target>Preferencias de Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences were changed." xml:space="preserve">
<source>Chat preferences were changed.</source>
<target>Las preferencias del chat han sido modificadas.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Chat profile" xml:space="preserve">
<source>Chat profile</source>
<target>Perfil de usuario</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat theme" xml:space="preserve">
<source>Chat theme</source>
<target>Tema de chat</target>
@@ -1493,7 +1568,7 @@
</trans-unit>
<trans-unit id="Confirm that you remember database passphrase to migrate it." xml:space="preserve">
<source>Confirm that you remember database passphrase to migrate it.</source>
<target>Para migrar confirma que recuerdas la frase de contraseña de la base de datos.</target>
<target>Para migrar la base de datos confirma que recuerdas la frase de contraseña.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Confirm upload" xml:space="preserve">
@@ -1740,6 +1815,11 @@ This is your own one-time link!</source>
<target>Versión Core: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<target>Esquina</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>¿Corregir el nombre a %@?</target>
@@ -1865,6 +1945,11 @@ This is your own one-time link!</source>
<target>Tiempo personalizado</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source>
<target>Forma personalizable de los mensajes.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customize theme" xml:space="preserve">
<source>Customize theme</source>
<target>Personalizar tema</target>
@@ -1946,7 +2031,7 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve">
<source>Database passphrase is different from saved in the keychain.</source>
<target>La contraseña es distinta a la almacenada en Keychain.</target>
<target>La contraseña es diferente a la almacenada en Keychain.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
@@ -2159,6 +2244,11 @@ This is your own one-time link!</source>
<target>¿Eliminar base de datos antigua?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve">
<source>Delete pending connection?</source>
<target>¿Eliminar conexión pendiente?</target>
@@ -2371,7 +2461,7 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Disconnect desktop?" xml:space="preserve">
<source>Disconnect desktop?</source>
<target>¿Desconectar ordenador?</target>
<target>¿Desconectar del ordenador?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Discover and join groups" xml:space="preserve">
@@ -2406,7 +2496,12 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Do not send history to new members." xml:space="preserve">
<source>Do not send history to new members.</source>
<target>No enviar historial a miembros nuevos.</target>
<target>No se envía el historial a los miembros nuevos.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not use credentials with proxy." xml:space="preserve">
<source>Do not use credentials with proxy.</source>
<target>No uses credenciales con proxy.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
@@ -2421,7 +2516,7 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Don't show again" xml:space="preserve">
<source>Don't show again</source>
<target>No mostrar de nuevo</target>
<target>No volver a mostrar</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Downgrade and open chat" xml:space="preserve">
@@ -2432,7 +2527,8 @@ This is your own one-time link!</source>
<trans-unit id="Download" xml:space="preserve">
<source>Download</source>
<target>Descargar</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Download errors" xml:space="preserve">
<source>Download errors</source>
@@ -2449,6 +2545,11 @@ This is your own one-time link!</source>
<target>Descargar archivo</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Download files" xml:space="preserve">
<source>Download files</source>
<target>Descargar archivos</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Downloaded" xml:space="preserve">
<source>Downloaded</source>
<target>Descargado</target>
@@ -2724,6 +2825,11 @@ This is your own one-time link!</source>
<target>Error al cambiar servidor</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<target>Error al cambiar el perfil de conexión</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Error al cambiar rol</target>
@@ -2734,6 +2840,11 @@ This is your own one-time link!</source>
<target>Error cambiando configuración</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<target>¡Error al cambiar a incógnito!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Error al conectar con el servidor de reenvío %@. Por favor, inténtalo más tarde.</target>
@@ -2854,6 +2965,11 @@ This is your own one-time link!</source>
<target>Error al cargar servidores %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error migrating settings" xml:space="preserve">
<source>Error migrating settings</source>
<target>Error al migrar la configuración</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<target>Error al abrir chat</target>
@@ -2862,7 +2978,7 @@ This is your own one-time link!</source>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Error al recibir archivo</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Error reconnecting server" xml:space="preserve">
<source>Error reconnecting server</source>
@@ -2954,10 +3070,15 @@ This is your own one-time link!</source>
<target>Error al parar SimpleX</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<target>Error al cambiar perfil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>¡Error al cambiar perfil!</target>
<note>No comment provided by engineer.</note>
<note>alertTitle</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
@@ -3002,8 +3123,7 @@ This is your own one-time link!</source>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>Error: %@</target>
<note>file error text
snd error text</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Error: URL is invalid" xml:space="preserve">
<source>Error: URL is invalid</source>
@@ -3090,6 +3210,13 @@ This is your own one-time link!</source>
<target>Error de archivo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File errors:&#10;%@" xml:space="preserve">
<source>File errors:
%@</source>
<target>Error(es) de archivo
%@</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve">
<source>File not found - most likely file was deleted or cancelled.</source>
<target>Archivo no encontrado, probablemente haya sido borrado o cancelado.</target>
@@ -3225,11 +3352,31 @@ This is your own one-time link!</source>
<target>Reenviar</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Forward %d message(s)?" xml:space="preserve">
<source>Forward %d message(s)?</source>
<target>¿Reenviar %d mensaje(s)?</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Forward and save messages" xml:space="preserve">
<source>Forward and save messages</source>
<target>Reenviar y guardar mensajes</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forward messages" xml:space="preserve">
<source>Forward messages</source>
<target>Reenviar mensajes</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Forward messages without files?" xml:space="preserve">
<source>Forward messages without files?</source>
<target>¿Reenviar mensajes sin los archivos?</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source>
<target>Desplazamiento de hasta 20 mensajes.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarded" xml:space="preserve">
<source>Forwarded</source>
<target>Reenviado</target>
@@ -3240,6 +3387,11 @@ This is your own one-time link!</source>
<target>Reenviado por</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding %lld messages" xml:space="preserve">
<source>Forwarding %lld messages</source>
<target>Reenviando %lld mensajes</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve">
<source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source>
<target>El servidor de reenvío %@ no ha podido conectarse al servidor de destino %@. Por favor, intentalo más tarde.</target>
@@ -3289,19 +3441,14 @@ Error: %2$@</target>
<target>Nombre completo (opcional)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Full name:" xml:space="preserve">
<source>Full name:</source>
<target>Nombre completo:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<target>Completamente descentralizado y sólo visible para los miembros.</target>
<target>Totalmente descentralizado. Visible sólo para los miembros.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully re-implemented - work in background!" xml:space="preserve">
<source>Fully re-implemented - work in background!</source>
<target>Completamente reimplementado: ¡funciona en segundo plano!</target>
<target>Totalmente revisado. ¡Funciona en segundo plano!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Further reduced battery usage" xml:space="preserve">
@@ -3539,6 +3686,11 @@ Error: %2$@</target>
<target>Servidores ICE (uno por línea)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="IP address" xml:space="preserve">
<source>IP address</source>
<target>Dirección IP</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
<target>Si no puedes reunirte en persona, muestra el código QR por videollamada o comparte el enlace.</target>
@@ -3614,6 +3766,13 @@ Error: %2$@</target>
<target>Importando archivo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage.
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>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<target>Entrega de mensajes mejorada</target>
@@ -4016,7 +4175,7 @@ This is your link for group %@!</source>
</trans-unit>
<trans-unit id="Local profile data only" xml:space="preserve">
<source>Local profile data only</source>
<target>Sólo datos del perfil local</target>
<target>Eliminar sólo el perfil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Lock after" xml:space="preserve">
@@ -4184,6 +4343,11 @@ This is your link for group %@!</source>
<target>Servidores de mensajes</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<target>Forma del mensaje</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>El autor del mensaje se mantiene privado.</target>
@@ -4234,6 +4398,11 @@ This is your link for group %@!</source>
<target>Mensajes enviados</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages were deleted after you selected them." xml:space="preserve">
<source>Messages were deleted after you selected them.</source>
<target>Los mensajes han sido borrados después de seleccionarlos.</target>
<note>alert message</note>
</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">
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
<target>Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo** con secreto perfecto hacía adelante, repudio y recuperación tras ataque.</target>
@@ -4389,6 +4558,16 @@ This is your link for group %@!</source>
<target>Código Nuevo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve">
<source>New SOCKS credentials will be used every time you start the app.</source>
<target>Se usarán credenciales SOCKS nuevas cada vez que inicies la aplicación.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve">
<source>New SOCKS credentials will be used for each server.</source>
<target>Se usarán credenciales SOCKS nuevas por cada servidor.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<target>Nuevo chat</target>
@@ -4509,6 +4688,16 @@ This is your link for group %@!</source>
<target>Sin conexión de red</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record speech" xml:space="preserve">
<source>No permission to record speech</source>
<target>Sin permiso para grabación de voz</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record video" xml:space="preserve">
<source>No permission to record video</source>
<target>Sin permiso para grabación de vídeo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>Sin permiso para grabar mensajes de voz</target>
@@ -4529,6 +4718,11 @@ This is your link for group %@!</source>
<target>Nada seleccionado</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Nothing to forward!" xml:space="preserve">
<source>Nothing to forward!</source>
<target>¡Nada para reenviar!</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Notificaciones</target>
@@ -4561,7 +4755,7 @@ This is your link for group %@!</source>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Old database" xml:space="preserve">
<source>Old database</source>
@@ -4752,6 +4946,13 @@ Requiere activación de la VPN.</target>
<target>Otros servidores %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other file errors:&#10;%@" xml:space="preserve">
<source>Other file errors:
%@</source>
<target>Otro(s) error(es) de archivo.
%@</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>Contador PING</target>
@@ -4787,6 +4988,11 @@ Requiere activación de la VPN.</target>
<target>¡Código de acceso guardado!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password" xml:space="preserve">
<source>Password</source>
<target>Contraseña</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password to show" xml:space="preserve">
<source>Password to show</source>
<target>Contraseña para hacerlo visible</target>
@@ -4936,9 +5142,14 @@ Error: %@</target>
<target>Interfaz en polaco</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Port" xml:space="preserve">
<source>Port</source>
<target>Puerto</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve">
<source>Possibly, certificate fingerprint in server address is incorrect</source>
<target>Posiblemente la huella digital del certificado en la dirección del servidor es incorrecta</target>
<target>Posiblemente la huella del certificado en la dirección del servidor es incorrecta</target>
<note>server test error</note>
</trans-unit>
<trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve">
@@ -5008,7 +5219,7 @@ Error: %@</target>
</trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve">
<source>Profile and server connections</source>
<target>Datos del perfil y conexiones</target>
<target>Eliminar perfil y conexiones</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile image" xml:space="preserve">
@@ -5021,16 +5232,6 @@ Error: %@</target>
<target>Forma de los perfiles</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<target>Nombre del perfil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<target>Nombre del perfil:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source>
<target>Contraseña del perfil</target>
@@ -5068,7 +5269,7 @@ Error: %@</target>
</trans-unit>
<trans-unit id="Prohibit sending SimpleX links." xml:space="preserve">
<source>Prohibit sending SimpleX links.</source>
<target>No permitir el envío de enlaces SimpleX.</target>
<target>No se permite enviar enlaces SimpleX.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Prohibit sending direct messages to members." xml:space="preserve">
@@ -5133,6 +5334,11 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Servidores con proxy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Proxy requires password" xml:space="preserve">
<source>Proxy requires password</source>
<target>El proxy requiere contraseña</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>Notificaciones automáticas</target>
@@ -5354,6 +5560,11 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Eliminar</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
<target>¿Eliminar archivo?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove image" xml:space="preserve">
<source>Remove image</source>
<target>Eliminar imagen</target>
@@ -5534,6 +5745,11 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Servidor SMP</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SOCKS proxy" xml:space="preserve">
<source>SOCKS proxy</source>
<target>Proxy SOCKS</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Safely receive files" xml:space="preserve">
<source>Safely receive files</source>
<target>Recibe archivos de forma segura</target>
@@ -5547,12 +5763,13 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<target>Guardar</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
<source>Save (and notify contacts)</source>
<target>Guardar (y notificar contactos)</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Save and notify contact" xml:space="preserve">
<source>Save and notify contact</source>
@@ -5579,11 +5796,6 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Guardar archivo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save auto-accept settings" xml:space="preserve">
<source>Save auto-accept settings</source>
<target>Guardar configuración de auto aceptar</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save group profile" xml:space="preserve">
<source>Save group profile</source>
<target>Guardar perfil de grupo</target>
@@ -5619,16 +5831,16 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>¿Guardar servidores?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save settings?" xml:space="preserve">
<source>Save settings?</source>
<target>¿Guardar configuración?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save welcome message?" xml:space="preserve">
<source>Save welcome message?</source>
<target>¿Guardar mensaje de bienvenida?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save your profile?" xml:space="preserve">
<source>Save your profile?</source>
<target>¿Guardar tu perfil?</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Saved" xml:space="preserve">
<source>Saved</source>
<target>Guardado</target>
@@ -5649,6 +5861,11 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Mensaje guardado</target>
<note>message info title</note>
</trans-unit>
<trans-unit id="Saving %lld messages" xml:space="preserve">
<source>Saving %lld messages</source>
<target>Guardando %lld mensajes</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scale" xml:space="preserve">
<source>Scale</source>
<target>Escala</target>
@@ -5729,6 +5946,11 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Seleccionar</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<target>Selecciona perfil de chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>Seleccionados %lld</target>
@@ -5776,7 +5998,7 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
</trans-unit>
<trans-unit id="Send direct message to connect" xml:space="preserve">
<source>Send direct message to connect</source>
<target>Envia un mensaje para conectar</target>
<target>Envía un mensaje para conectar</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Send disappearing message" xml:space="preserve">
@@ -5841,13 +6063,13 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
</trans-unit>
<trans-unit id="Send up to 100 last messages to new members." xml:space="preserve">
<source>Send up to 100 last messages to new members.</source>
<target>Enviar hasta 100 últimos mensajes a los miembros nuevos.</target>
<target>Se envían hasta 100 mensajes más recientes a los miembros nuevos.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>El remitente ha cancelado la transferencia de archivos.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Sender may have deleted the connection request." xml:space="preserve">
<source>Sender may have deleted the connection request.</source>
@@ -5944,6 +6166,11 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Mediante proxy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<target>Servidor</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server address" xml:space="preserve">
<source>Server address</source>
<target>Dirección del servidor</target>
@@ -5971,7 +6198,7 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
</trans-unit>
<trans-unit id="Server test failed!" xml:space="preserve">
<source>Server test failed!</source>
<target>¡Error en prueba del servidor!</target>
<target>¡Prueba no superada!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server type" xml:space="preserve">
@@ -6064,6 +6291,11 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Configuración</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Settings were changed." xml:space="preserve">
<source>Settings were changed.</source>
<target>La configuración ha sido modificada.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Shape profile images" xml:space="preserve">
<source>Shape profile images</source>
<target>Dar forma a las imágenes de perfil</target>
@@ -6099,6 +6331,11 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Compartir enlace</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<target>Comparte perfil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Comparte este enlace de un solo uso</target>
@@ -6234,6 +6471,11 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Invitación SimpleX de un uso</target>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Modo incógnito simplificado</target>
@@ -6264,6 +6506,11 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Suave</target>
<note>blur media</note>
</trans-unit>
<trans-unit id="Some app settings were not migrated." xml:space="preserve">
<source>Some app settings were not migrated.</source>
<target>Algunas configuraciones de la app no han sido migradas.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some file(s) were not exported:" xml:space="preserve">
<source>Some file(s) were not exported:</source>
<target>Algunos archivos no han sido exportados:</target>
@@ -6404,6 +6651,16 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>Soporte SimpleX Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>Sistema</target>
@@ -6439,6 +6696,11 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<target>Cola</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Tomar foto</target>
@@ -6486,7 +6748,7 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
</trans-unit>
<trans-unit id="Test failed at step %@." xml:space="preserve">
<source>Test failed at step %@.</source>
<target>La prueba ha fallado en el paso %@.</target>
<target>Prueba no superada en el paso %@.</target>
<note>server test failure</note>
</trans-unit>
<trans-unit id="Test server" xml:space="preserve">
@@ -6501,7 +6763,7 @@ Actívalo en ajustes de *Servidores y Redes*.</target>
</trans-unit>
<trans-unit id="Tests failed!" xml:space="preserve">
<source>Tests failed!</source>
<target>¡Pruebas fallidas!</target>
<target>¡Pruebas no superadas!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve">
@@ -6631,6 +6893,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target>
<target>El texto pegado no es un enlace SimpleX.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve">
<source>The uploaded database archive will be permanently removed from the servers.</source>
<target>El archivo de bases de datos subido será eliminado permanentemente de los servidores.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Themes" xml:space="preserve">
<source>Themes</source>
<target>Temas</target>
@@ -6758,6 +7025,16 @@ You will be prompted to complete authentication before this feature is enabled.<
Se te pedirá que completes la autenticación antes de activar esta función.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve">
<source>To record speech please grant permission to use Microphone.</source>
<target>Para grabación de voz, por favor concede el permiso para usar el micrófono.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record video please grant permission to use Camera." xml:space="preserve">
<source>To record video please grant permission to use Camera.</source>
<target>Para grabación de vídeo, por favor concede el permiso para usar la cámara.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve">
<source>To record voice message please grant permission to use Microphone.</source>
<target>Para grabar el mensaje de voz concede permiso para usar el micrófono.</target>
@@ -6911,7 +7188,7 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t
<trans-unit id="Unknown servers!" xml:space="preserve">
<source>Unknown servers!</source>
<target>¡Servidores desconocidos!</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
@@ -7025,6 +7302,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión
<target>Usar hosts .onion</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SOCKS proxy" xml:space="preserve">
<source>Use SOCKS proxy</source>
<target>Usar proxy SOCKS</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
<source>Use SimpleX Chat servers?</source>
<target>¿Usar servidores SimpleX Chat?</target>
@@ -7090,16 +7372,16 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión
<target>Usa la aplicación con una sola mano.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User profile" xml:space="preserve">
<source>User profile</source>
<target>Perfil de usuario</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User selection" xml:space="preserve">
<source>User selection</source>
<target>Selección de usuarios</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<target>Nombre de usuario</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
<source>Using SimpleX Chat servers.</source>
<target>Usar servidores SimpleX Chat.</target>
@@ -7333,7 +7615,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
<target>Sin Tor o VPN, tu dirección IP será visible para estos servidores XFTP: %@.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
@@ -7360,11 +7642,6 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión
<target>Servidor XFTP</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You" xml:space="preserve">
<source>You</source>
<target>Tú</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
<source>You **must not** use the same database on two devices.</source>
<target>**No debes** usar la misma base de datos en dos dispositivos.</target>
@@ -7449,7 +7726,7 @@ Repeat join request?</source>
</trans-unit>
<trans-unit id="You can change it in Appearance settings." xml:space="preserve">
<source>You can change it in Appearance settings.</source>
<target>Puede cambiarlo desde el menú Apariencia.</target>
<target>Puedes cambiar la posición de la barra desde el menú Apariencia.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You can create it later" xml:space="preserve">
@@ -7714,11 +7991,21 @@ Repeat connection request?</source>
<target>La base de datos no está cifrada - establece una contraseña para cifrarla.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat preferences" xml:space="preserve">
<source>Your chat preferences</source>
<target>Tus preferencias de chat</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>Mis perfiles</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<target>Tu conexión ha sido trasladada a %@ pero ha ocurrido un error inesperado al redirigirte al perfil.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>El contacto ha enviado un archivo mayor al máximo admitido (%@).</target>
@@ -7734,6 +8021,11 @@ Repeat connection request?</source>
<target>Tus contactos permanecerán conectados.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve">
<source>Your credentials may be sent unencrypted.</source>
<target>Tus credenciales podrían ser enviadas sin cifrar.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve">
<source>Your current chat database will be DELETED and REPLACED with the imported one.</source>
<target>La base de datos actual será ELIMINADA y SUSTITUIDA por la importada.</target>
@@ -7764,13 +8056,16 @@ Repeat connection request?</source>
<target>El perfil **%@** será compartido.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
<target>Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos.
Los servidores SimpleX no pueden ver tu perfil.</target>
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
<target>Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve">
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
<target>Tu perfil ha sido modificado. Si lo guardas la actualización será enviada a todos tus contactos.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>Tu perfil, contactos y mensajes se almacenan en tu dispositivo.</target>
@@ -7788,7 +8083,7 @@ Los servidores SimpleX no pueden ver tu perfil.</target>
</trans-unit>
<trans-unit id="Your server address" xml:space="preserve">
<source>Your server address</source>
<target>Dirección de tu servidor</target>
<target>Dirección del servidor</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your settings" xml:space="preserve">
@@ -8500,7 +8795,7 @@ Los servidores SimpleX no pueden ver tu perfil.</target>
</trans-unit>
<trans-unit id="removed profile picture" xml:space="preserve">
<source>removed profile picture</source>
<target>imagen de perfil eliminada</target>
<target>ha eliminado la imagen del perfil</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="removed you" xml:space="preserve">
@@ -8564,7 +8859,7 @@ last received msg: %2$@</source>
</trans-unit>
<trans-unit id="set new profile picture" xml:space="preserve">
<source>set new profile picture</source>
<target>nueva imagen de perfil</target>
<target>tiene nueva imagen del perfil</target>
<note>profile update event chat item</note>
</trans-unit>
<trans-unit id="standard end-to-end encryption" xml:space="preserve">
@@ -8912,7 +9207,7 @@ last received msg: %2$@</source>
</trans-unit>
<trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve">
<source>Database passphrase is different from saved in the keychain.</source>
<target>La contraseña de la base de datos es distinta a la almacenada en keychain.</target>
<target>La contraseña de la base de datos es diferente a la almacenada en keychain.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Database passphrase is required to open chat." xml:space="preserve">
@@ -133,6 +133,10 @@
<target>%@ haluaa muodostaa yhteyden!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<note>No comment provided by engineer.</note>
@@ -152,11 +156,31 @@
<target>%d päivää</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve">
<source>%d file(s) are still being downloaded.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) failed to download." xml:space="preserve">
<source>%d file(s) failed to download.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were deleted." xml:space="preserve">
<source>%d file(s) were deleted.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve">
<source>%d file(s) were not downloaded.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d hours" xml:space="preserve">
<source>%d hours</source>
<target>%d tuntia</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d messages not forwarded" xml:space="preserve">
<source>%d messages not forwarded</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="%d min" xml:space="preserve">
<source>%d min</source>
<target>%d min</target>
@@ -686,7 +710,7 @@
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<note>No comment provided by engineer.</note>
<note>profile dropdown</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source>
@@ -868,6 +892,10 @@
<target>Sovelluksen pääsykoodi korvataan itsetuhoutuvalla pääsykoodilla.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App session" xml:space="preserve">
<source>App session</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App version" xml:space="preserve">
<source>App version</source>
<target>Sovellusversio</target>
@@ -967,6 +995,10 @@
<target>Hyväksy kuvat automaattisesti</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
<source>Back</source>
<target>Takaisin</target>
@@ -990,10 +1022,18 @@
<target>Virheellinen viestin tarkiste</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>Parempia viestejä</target>
@@ -1003,6 +1043,18 @@
<source>Better networking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Black" xml:space="preserve">
<source>Black</source>
<note>No comment provided by engineer.</note>
@@ -1120,7 +1172,7 @@
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Peruuta</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve">
<source>Cancel migration</source>
@@ -1138,7 +1190,7 @@
<trans-unit id="Cannot receive file" xml:space="preserve">
<source>Cannot receive file</source>
<target>Tiedostoa ei voi vastaanottaa</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
@@ -1259,6 +1311,15 @@
<target>Chat-asetukset</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences were changed." xml:space="preserve">
<source>Chat preferences were changed.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Chat profile" xml:space="preserve">
<source>Chat profile</source>
<target>Käyttäjäprofiili</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat theme" xml:space="preserve">
<source>Chat theme</source>
<note>No comment provided by engineer.</note>
@@ -1616,6 +1677,10 @@ This is your own one-time link!</source>
<target>Ydinversio: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
@@ -1731,6 +1796,10 @@ This is your own one-time link!</source>
<target>Mukautettu aika</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customize theme" xml:space="preserve">
<source>Customize theme</source>
<note>No comment provided by engineer.</note>
@@ -2017,6 +2086,10 @@ This is your own one-time link!</source>
<target>Poista vanha tietokanta?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve">
<source>Delete pending connection?</source>
<target>Poistetaanko odottava yhteys?</target>
@@ -2248,6 +2321,10 @@ This is your own one-time link!</source>
<source>Do not send history to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not use credentials with proxy." xml:space="preserve">
<source>Do not use credentials with proxy.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>Älä luo osoitetta</target>
@@ -2270,7 +2347,8 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Download" xml:space="preserve">
<source>Download</source>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Download errors" xml:space="preserve">
<source>Download errors</source>
@@ -2285,6 +2363,10 @@ This is your own one-time link!</source>
<target>Lataa tiedosto</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Download files" xml:space="preserve">
<source>Download files</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Downloaded" xml:space="preserve">
<source>Downloaded</source>
<note>No comment provided by engineer.</note>
@@ -2544,6 +2626,10 @@ This is your own one-time link!</source>
<target>Virhe osoitteenvaihdossa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Virhe roolin vaihdossa</target>
@@ -2554,6 +2640,10 @@ This is your own one-time link!</source>
<target>Virhe asetuksen muuttamisessa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -2669,6 +2759,10 @@ This is your own one-time link!</source>
<target>Virhe %@-palvelimien lataamisessa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error migrating settings" xml:space="preserve">
<source>Error migrating settings</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
@@ -2676,7 +2770,7 @@ This is your own one-time link!</source>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Virhe tiedoston vastaanottamisessa</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Error reconnecting server" xml:space="preserve">
<source>Error reconnecting server</source>
@@ -2762,10 +2856,14 @@ This is your own one-time link!</source>
<target>Virhe keskustelun lopettamisessa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Virhe profiilin vaihdossa!</target>
<note>No comment provided by engineer.</note>
<note>alertTitle</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
@@ -2808,8 +2906,7 @@ This is your own one-time link!</source>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>Virhe: %@</target>
<note>file error text
snd error text</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Error: URL is invalid" xml:space="preserve">
<source>Error: URL is invalid</source>
@@ -2890,6 +2987,11 @@ This is your own one-time link!</source>
<source>File error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File errors:&#10;%@" xml:space="preserve">
<source>File errors:
%@</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve">
<source>File not found - most likely file was deleted or cancelled.</source>
<note>file error text</note>
@@ -3016,10 +3118,26 @@ This is your own one-time link!</source>
<source>Forward</source>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Forward %d message(s)?" xml:space="preserve">
<source>Forward %d message(s)?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Forward and save messages" xml:space="preserve">
<source>Forward and save messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forward messages" xml:space="preserve">
<source>Forward messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Forward messages without files?" xml:space="preserve">
<source>Forward messages without files?</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarded" xml:space="preserve">
<source>Forwarded</source>
<note>No comment provided by engineer.</note>
@@ -3028,6 +3146,10 @@ This is your own one-time link!</source>
<source>Forwarded from</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding %lld messages" xml:space="preserve">
<source>Forwarding %lld messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve">
<source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -3069,11 +3191,6 @@ Error: %2$@</source>
<target>Koko nimi (valinnainen)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Full name:" xml:space="preserve">
<source>Full name:</source>
<target>Koko nimi:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<note>No comment provided by engineer.</note>
@@ -3311,6 +3428,10 @@ Error: %2$@</source>
<target>ICE-palvelimet (yksi per rivi)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="IP address" xml:space="preserve">
<source>IP address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
<target>Jos et voi tavata henkilökohtaisesti, näytä QR-koodi videopuhelussa tai jaa linkki.</target>
@@ -3383,6 +3504,11 @@ Error: %2$@</source>
<source>Importing archive</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<note>No comment provided by engineer.</note>
@@ -3918,6 +4044,10 @@ This is your link for group %@!</source>
<source>Message servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<note>No comment provided by engineer.</note>
@@ -3961,6 +4091,10 @@ This is your link for group %@!</source>
<source>Messages sent</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages were deleted after you selected them." xml:space="preserve">
<source>Messages were deleted after you selected them.</source>
<note>alert message</note>
</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">
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
<note>No comment provided by engineer.</note>
@@ -4103,6 +4237,14 @@ This is your link for group %@!</source>
<target>Uusi pääsykoodi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve">
<source>New SOCKS credentials will be used every time you start the app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve">
<source>New SOCKS credentials will be used for each server.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
@@ -4216,6 +4358,14 @@ This is your link for group %@!</source>
<source>No network connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record speech" xml:space="preserve">
<source>No permission to record speech</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record video" xml:space="preserve">
<source>No permission to record video</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>Ei lupaa ääniviestin tallentamiseen</target>
@@ -4234,6 +4384,10 @@ This is your link for group %@!</source>
<source>Nothing selected</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Nothing to forward!" xml:space="preserve">
<source>Nothing to forward!</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Ilmoitukset</target>
@@ -4265,7 +4419,7 @@ This is your link for group %@!</source>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Old database" xml:space="preserve">
<source>Old database</source>
@@ -4444,6 +4598,11 @@ Edellyttää VPN:n sallimista.</target>
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other file errors:&#10;%@" xml:space="preserve">
<source>Other file errors:
%@</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>PING-määrä</target>
@@ -4479,6 +4638,10 @@ Edellyttää VPN:n sallimista.</target>
<target>Pääsykoodi asetettu!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password" xml:space="preserve">
<source>Password</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password to show" xml:space="preserve">
<source>Password to show</source>
<target>Salasana näytettäväksi</target>
@@ -4615,6 +4778,10 @@ Error: %@</source>
<target>Puolalainen käyttöliittymä</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Port" xml:space="preserve">
<source>Port</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve">
<source>Possibly, certificate fingerprint in server address is incorrect</source>
<target>Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen</target>
@@ -4693,14 +4860,6 @@ Error: %@</source>
<source>Profile images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source>
<target>Profiilin salasana</target>
@@ -4796,6 +4955,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Proxied servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Proxy requires password" xml:space="preserve">
<source>Proxy requires password</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>Push-ilmoitukset</target>
@@ -5002,6 +5165,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Poista</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove image" xml:space="preserve">
<source>Remove image</source>
<note>No comment provided by engineer.</note>
@@ -5169,6 +5336,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>SMP server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SOCKS proxy" xml:space="preserve">
<source>SOCKS proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Safely receive files" xml:space="preserve">
<source>Safely receive files</source>
<note>No comment provided by engineer.</note>
@@ -5180,12 +5351,13 @@ Enable in *Network &amp; servers* settings.</source>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<target>Tallenna</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
<source>Save (and notify contacts)</source>
<target>Tallenna (ja ilmoita kontakteille)</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Save and notify contact" xml:space="preserve">
<source>Save and notify contact</source>
@@ -5211,11 +5383,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Tallenna arkisto</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save auto-accept settings" xml:space="preserve">
<source>Save auto-accept settings</source>
<target>Tallenna automaattisen hyväksynnän asetukset</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save group profile" xml:space="preserve">
<source>Save group profile</source>
<target>Tallenna ryhmäprofiili</target>
@@ -5251,16 +5418,15 @@ Enable in *Network &amp; servers* settings.</source>
<target>Tallenna palvelimet?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save settings?" xml:space="preserve">
<source>Save settings?</source>
<target>Tallenna asetukset?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save welcome message?" xml:space="preserve">
<source>Save welcome message?</source>
<target>Tallenna tervetuloviesti?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save your profile?" xml:space="preserve">
<source>Save your profile?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Saved" xml:space="preserve">
<source>Saved</source>
<note>No comment provided by engineer.</note>
@@ -5278,6 +5444,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Saved message</source>
<note>message info title</note>
</trans-unit>
<trans-unit id="Saving %lld messages" xml:space="preserve">
<source>Saving %lld messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scale" xml:space="preserve">
<source>Scale</source>
<note>No comment provided by engineer.</note>
@@ -5351,6 +5521,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Valitse</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<note>No comment provided by engineer.</note>
@@ -5461,7 +5635,7 @@ Enable in *Network &amp; servers* settings.</source>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>Lähettäjä peruutti tiedoston siirron.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Sender may have deleted the connection request." xml:space="preserve">
<source>Sender may have deleted the connection request.</source>
@@ -5553,6 +5727,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Sent via proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server address" xml:space="preserve">
<source>Server address</source>
<note>No comment provided by engineer.</note>
@@ -5662,6 +5840,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Asetukset</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Settings were changed." xml:space="preserve">
<source>Settings were changed.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Shape profile images" xml:space="preserve">
<source>Shape profile images</source>
<note>No comment provided by engineer.</note>
@@ -5695,6 +5877,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>Jaa linkki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<note>No comment provided by engineer.</note>
@@ -5821,6 +6007,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>SimpleX-kertakutsu</target>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<note>No comment provided by engineer.</note>
@@ -5848,6 +6038,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Soft</source>
<note>blur media</note>
</trans-unit>
<trans-unit id="Some app settings were not migrated." xml:space="preserve">
<source>Some app settings were not migrated.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some file(s) were not exported:" xml:space="preserve">
<source>Some file(s) were not exported:</source>
<note>No comment provided by engineer.</note>
@@ -5976,6 +6170,14 @@ Enable in *Network &amp; servers* settings.</source>
<target>SimpleX Chat tuki</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>Järjestelmä</target>
@@ -6010,6 +6212,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Ota kuva</target>
@@ -6193,6 +6399,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</t
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve">
<source>The uploaded database archive will be permanently removed from the servers.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Themes" xml:space="preserve">
<source>Themes</source>
<note>No comment provided by engineer.</note>
@@ -6309,6 +6519,14 @@ You will be prompted to complete authentication before this feature is enabled.<
Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus otetaan käyttöön.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve">
<source>To record speech please grant permission to use Microphone.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record video please grant permission to use Camera." xml:space="preserve">
<source>To record video please grant permission to use Camera.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve">
<source>To record voice message please grant permission to use Microphone.</source>
<target>Jos haluat nauhoittaa ääniviestin, anna lupa käyttää mikrofonia.</target>
@@ -6450,7 +6668,7 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote
</trans-unit>
<trans-unit id="Unknown servers!" xml:space="preserve">
<source>Unknown servers!</source>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
@@ -6555,6 +6773,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Käytä .onion-isäntiä</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SOCKS proxy" xml:space="preserve">
<source>Use SOCKS proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
<source>Use SimpleX Chat servers?</source>
<target>Käytä SimpleX Chat palvelimia?</target>
@@ -6614,15 +6836,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<source>Use the app with one hand.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User profile" xml:space="preserve">
<source>User profile</source>
<target>Käyttäjäprofiili</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User selection" xml:space="preserve">
<source>User selection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
<source>Using SimpleX Chat servers.</source>
<target>Käyttää SimpleX Chat -palvelimia.</target>
@@ -6835,7 +7056,7 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
</trans-unit>
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
@@ -6859,11 +7080,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<source>XFTP server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You" xml:space="preserve">
<source>You</source>
<target>Sinä</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
<source>You **must not** use the same database on two devices.</source>
<note>No comment provided by engineer.</note>
@@ -7189,11 +7405,19 @@ Repeat connection request?</source>
<target>Keskustelut-tietokantasi ei ole salattu - aseta tunnuslause sen salaamiseksi.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat preferences" xml:space="preserve">
<source>Your chat preferences</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>Keskusteluprofiilisi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@).</target>
@@ -7209,6 +7433,10 @@ Repeat connection request?</source>
<target>Kontaktisi pysyvät yhdistettyinä.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve">
<source>Your credentials may be sent unencrypted.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve">
<source>Your current chat database will be DELETED and REPLACED with the imported one.</source>
<target>Nykyinen keskustelut-tietokantasi poistetaan ja korvataan tuodulla tietokannalla.</target>
@@ -7238,13 +7466,15 @@ Repeat connection request?</source>
<target>Profiilisi **%@** jaetaan.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
<target>Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa.
SimpleX-palvelimet eivät näe profiiliasi.</target>
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
<target>Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. SimpleX-palvelimet eivät näe profiiliasi.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve">
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi.</target>
@@ -137,6 +137,10 @@
<target>%@ veut se connecter !</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ et %lld membres</target>
@@ -157,11 +161,31 @@
<target>%d jours</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve">
<source>%d file(s) are still being downloaded.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) failed to download." xml:space="preserve">
<source>%d file(s) failed to download.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were deleted." xml:space="preserve">
<source>%d file(s) were deleted.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve">
<source>%d file(s) were not downloaded.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d hours" xml:space="preserve">
<source>%d hours</source>
<target>%d heures</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d messages not forwarded" xml:space="preserve">
<source>%d messages not forwarded</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="%d min" xml:space="preserve">
<source>%d min</source>
<target>%d min</target>
@@ -723,7 +747,7 @@
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<target>Tous les profiles</target>
<note>No comment provided by engineer.</note>
<note>profile dropdown</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source>
@@ -915,6 +939,10 @@
<target>Le code d'accès de l'application est remplacé par un code d'autodestruction.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App session" xml:space="preserve">
<source>App session</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App version" xml:space="preserve">
<source>App version</source>
<target>Version de l'app</target>
@@ -1020,6 +1048,10 @@
<target>Images auto-acceptées</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
<source>Back</source>
<target>Retour</target>
@@ -1045,11 +1077,19 @@
<target>Mauvais hash de message</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<target>Des groupes plus performants</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>Meilleurs messages</target>
@@ -1060,6 +1100,18 @@
<target>Meilleure gestion de réseau</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Black" xml:space="preserve">
<source>Black</source>
<target>Noir</target>
@@ -1193,7 +1245,7 @@
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Annuler</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve">
<source>Cancel migration</source>
@@ -1213,7 +1265,7 @@
<trans-unit id="Cannot receive file" xml:space="preserve">
<source>Cannot receive file</source>
<target>Impossible de recevoir le fichier</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
@@ -1341,6 +1393,15 @@
<target>Préférences de chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences were changed." xml:space="preserve">
<source>Chat preferences were changed.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Chat profile" xml:space="preserve">
<source>Chat profile</source>
<target>Profil d'utilisateur</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat theme" xml:space="preserve">
<source>Chat theme</source>
<target>Thème de chat</target>
@@ -1740,6 +1801,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Version du cœur : v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Corriger le nom pour %@ ?</target>
@@ -1865,6 +1930,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Délai personnalisé</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customize theme" xml:space="preserve">
<source>Customize theme</source>
<target>Personnaliser le thème</target>
@@ -2159,6 +2228,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Supprimer l'ancienne base de données?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve">
<source>Delete pending connection?</source>
<target>Supprimer la connexion en attente ?</target>
@@ -2409,6 +2482,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Ne pas envoyer d'historique aux nouveaux membres.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not use credentials with proxy." xml:space="preserve">
<source>Do not use credentials with proxy.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>Ne pas créer d'adresse</target>
@@ -2432,7 +2509,8 @@ Il s'agit de votre propre lien unique !</target>
<trans-unit id="Download" xml:space="preserve">
<source>Download</source>
<target>Télécharger</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Download errors" xml:space="preserve">
<source>Download errors</source>
@@ -2449,6 +2527,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Télécharger le fichier</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Download files" xml:space="preserve">
<source>Download files</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Downloaded" xml:space="preserve">
<source>Downloaded</source>
<target>Téléchargé</target>
@@ -2724,6 +2806,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Erreur de changement d'adresse</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Erreur lors du changement de rôle</target>
@@ -2734,6 +2820,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Erreur de changement de paramètre</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Erreur de connexion au serveur de redirection %@. Veuillez réessayer plus tard.</target>
@@ -2854,6 +2944,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Erreur lors du chargement des serveurs %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error migrating settings" xml:space="preserve">
<source>Error migrating settings</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<target>Erreur lors de l'ouverture du chat</target>
@@ -2862,7 +2956,7 @@ Il s'agit de votre propre lien unique !</target>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Erreur lors de la réception du fichier</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Error reconnecting server" xml:space="preserve">
<source>Error reconnecting server</source>
@@ -2954,10 +3048,14 @@ Il s'agit de votre propre lien unique !</target>
<target>Erreur lors de l'arrêt du chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Erreur lors du changement de profil !</target>
<note>No comment provided by engineer.</note>
<note>alertTitle</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
@@ -3002,8 +3100,7 @@ Il s'agit de votre propre lien unique !</target>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>Erreur: %@</target>
<note>file error text
snd error text</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Error: URL is invalid" xml:space="preserve">
<source>Error: URL is invalid</source>
@@ -3090,6 +3187,11 @@ Il s'agit de votre propre lien unique !</target>
<target>Erreur de fichier</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File errors:&#10;%@" xml:space="preserve">
<source>File errors:
%@</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve">
<source>File not found - most likely file was deleted or cancelled.</source>
<target>Fichier introuvable - le fichier a probablement été supprimé ou annulé.</target>
@@ -3225,11 +3327,27 @@ Il s'agit de votre propre lien unique !</target>
<target>Transférer</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Forward %d message(s)?" xml:space="preserve">
<source>Forward %d message(s)?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Forward and save messages" xml:space="preserve">
<source>Forward and save messages</source>
<target>Transférer et sauvegarder des messages</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forward messages" xml:space="preserve">
<source>Forward messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Forward messages without files?" xml:space="preserve">
<source>Forward messages without files?</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarded" xml:space="preserve">
<source>Forwarded</source>
<target>Transféré</target>
@@ -3240,6 +3358,10 @@ Il s'agit de votre propre lien unique !</target>
<target>Transféré depuis</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding %lld messages" xml:space="preserve">
<source>Forwarding %lld messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve">
<source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source>
<target>Le serveur de redirection %@ n'a pas réussi à se connecter au serveur de destination %@. Veuillez réessayer plus tard.</target>
@@ -3289,11 +3411,6 @@ Erreur: %2$@</target>
<target>Nom complet (optionnel)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Full name:" xml:space="preserve">
<source>Full name:</source>
<target>Nom complet :</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<target>Entièrement décentralisé visible que par ses membres.</target>
@@ -3539,6 +3656,10 @@ Erreur: %2$@</target>
<target>Serveurs ICE (un par ligne)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="IP address" xml:space="preserve">
<source>IP address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
<target>Si vous ne pouvez pas vous rencontrer en personne, montrez le code QR lors d'un appel vidéo ou partagez le lien.</target>
@@ -3614,6 +3735,11 @@ Erreur: %2$@</target>
<target>Importation de l'archive</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<target>Amélioration de la transmission des messages</target>
@@ -4184,6 +4310,10 @@ Voici votre lien pour le groupe %@ !</target>
<target>Serveurs de messages</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>La source du message reste privée.</target>
@@ -4234,6 +4364,10 @@ Voici votre lien pour le groupe %@ !</target>
<target>Messages envoyés</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages were deleted after you selected them." xml:space="preserve">
<source>Messages were deleted after you selected them.</source>
<note>alert message</note>
</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">
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
<target>Les messages, fichiers et appels sont protégés par un chiffrement **de bout en bout** avec une confidentialité persistante, une répudiation et une récupération en cas d'effraction.</target>
@@ -4389,6 +4523,14 @@ Voici votre lien pour le groupe %@ !</target>
<target>Nouveau code d'accès</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve">
<source>New SOCKS credentials will be used every time you start the app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve">
<source>New SOCKS credentials will be used for each server.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<target>Nouveau chat</target>
@@ -4509,6 +4651,14 @@ Voici votre lien pour le groupe %@ !</target>
<target>Pas de connexion au réseau</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record speech" xml:space="preserve">
<source>No permission to record speech</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record video" xml:space="preserve">
<source>No permission to record video</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>Pas l'autorisation d'enregistrer un message vocal</target>
@@ -4529,6 +4679,10 @@ Voici votre lien pour le groupe %@ !</target>
<target>Aucune sélection</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Nothing to forward!" xml:space="preserve">
<source>Nothing to forward!</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Notifications</target>
@@ -4561,7 +4715,7 @@ Voici votre lien pour le groupe %@ !</target>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Old database" xml:space="preserve">
<source>Old database</source>
@@ -4752,6 +4906,11 @@ Nécessite l'activation d'un VPN.</target>
<target>Autres serveurs %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other file errors:&#10;%@" xml:space="preserve">
<source>Other file errors:
%@</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>Nombre de PING</target>
@@ -4787,6 +4946,10 @@ Nécessite l'activation d'un VPN.</target>
<target>Code d'accès défini !</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password" xml:space="preserve">
<source>Password</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password to show" xml:space="preserve">
<source>Password to show</source>
<target>Mot de passe à entrer</target>
@@ -4936,6 +5099,10 @@ Erreur: %@</target>
<target>Interface en polonais</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Port" xml:space="preserve">
<source>Port</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve">
<source>Possibly, certificate fingerprint in server address is incorrect</source>
<target>Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte</target>
@@ -5021,16 +5188,6 @@ Erreur: %@</target>
<target>Images de profil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<target>Nom du profil</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<target>Nom du profil :</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source>
<target>Mot de passe de profil</target>
@@ -5133,6 +5290,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Serveurs routés via des proxy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Proxy requires password" xml:space="preserve">
<source>Proxy requires password</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>Notifications push</target>
@@ -5354,6 +5515,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Supprimer</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove image" xml:space="preserve">
<source>Remove image</source>
<target>Enlever l'image</target>
@@ -5534,6 +5699,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Serveur SMP</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SOCKS proxy" xml:space="preserve">
<source>SOCKS proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Safely receive files" xml:space="preserve">
<source>Safely receive files</source>
<target>Réception de fichiers en toute sécurité</target>
@@ -5547,12 +5716,13 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<target>Enregistrer</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
<source>Save (and notify contacts)</source>
<target>Enregistrer (et en informer les contacts)</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Save and notify contact" xml:space="preserve">
<source>Save and notify contact</source>
@@ -5579,11 +5749,6 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Enregistrer l'archive</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save auto-accept settings" xml:space="preserve">
<source>Save auto-accept settings</source>
<target>Enregistrer les paramètres de validation automatique</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save group profile" xml:space="preserve">
<source>Save group profile</source>
<target>Enregistrer le profil du groupe</target>
@@ -5619,16 +5784,15 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Enregistrer les serveurs?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save settings?" xml:space="preserve">
<source>Save settings?</source>
<target>Enregistrer les paramètres?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save welcome message?" xml:space="preserve">
<source>Save welcome message?</source>
<target>Enregistrer le message d'accueil?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save your profile?" xml:space="preserve">
<source>Save your profile?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Saved" xml:space="preserve">
<source>Saved</source>
<target>Enregistré</target>
@@ -5649,6 +5813,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Message enregistré</target>
<note>message info title</note>
</trans-unit>
<trans-unit id="Saving %lld messages" xml:space="preserve">
<source>Saving %lld messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scale" xml:space="preserve">
<source>Scale</source>
<target>Échelle</target>
@@ -5729,6 +5897,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Choisir</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>%lld sélectionné(s)</target>
@@ -5847,7 +6019,7 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>L'expéditeur a annulé le transfert de fichiers.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Sender may have deleted the connection request." xml:space="preserve">
<source>Sender may have deleted the connection request.</source>
@@ -5944,6 +6116,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Envoyé via le proxy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server address" xml:space="preserve">
<source>Server address</source>
<target>Adresse du serveur</target>
@@ -6064,6 +6240,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Paramètres</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Settings were changed." xml:space="preserve">
<source>Settings were changed.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Shape profile images" xml:space="preserve">
<source>Shape profile images</source>
<target>Images de profil modelable</target>
@@ -6099,6 +6279,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Partager le lien</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Partager ce lien d'invitation unique</target>
@@ -6234,6 +6418,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Invitation unique SimpleX</target>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Mode incognito simplifié</target>
@@ -6264,6 +6452,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Léger</target>
<note>blur media</note>
</trans-unit>
<trans-unit id="Some app settings were not migrated." xml:space="preserve">
<source>Some app settings were not migrated.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some file(s) were not exported:" xml:space="preserve">
<source>Some file(s) were not exported:</source>
<target>Certains fichiers n'ont pas été exportés:</target>
@@ -6404,6 +6596,14 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>Supporter SimpleX Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>Système</target>
@@ -6439,6 +6639,10 @@ Activez-le dans les paramètres *Réseau et serveurs*.</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Prendre une photo</target>
@@ -6631,6 +6835,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise.
<target>Le texte collé n'est pas un lien SimpleX.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve">
<source>The uploaded database archive will be permanently removed from the servers.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Themes" xml:space="preserve">
<source>Themes</source>
<target>Thèmes</target>
@@ -6758,6 +6966,14 @@ You will be prompted to complete authentication before this feature is enabled.<
Vous serez invité à confirmer l'authentification avant que cette fonction ne soit activée.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve">
<source>To record speech please grant permission to use Microphone.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record video please grant permission to use Camera." xml:space="preserve">
<source>To record video please grant permission to use Camera.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve">
<source>To record voice message please grant permission to use Microphone.</source>
<target>Pour enregistrer un message vocal, veuillez accorder la permission d'utiliser le microphone.</target>
@@ -6911,7 +7127,7 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s
<trans-unit id="Unknown servers!" xml:space="preserve">
<source>Unknown servers!</source>
<target>Serveurs inconnus!</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
@@ -7025,6 +7241,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien
<target>Utiliser les hôtes .onions</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SOCKS proxy" xml:space="preserve">
<source>Use SOCKS proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
<source>Use SimpleX Chat servers?</source>
<target>Utiliser les serveurs SimpleX Chat?</target>
@@ -7090,16 +7310,15 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien
<target>Utiliser l'application d'une main.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User profile" xml:space="preserve">
<source>User profile</source>
<target>Profil d'utilisateur</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User selection" xml:space="preserve">
<source>User selection</source>
<target>Sélection de l'utilisateur</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
<source>Using SimpleX Chat servers.</source>
<target>Vous utilisez les serveurs SimpleX.</target>
@@ -7333,7 +7552,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
<target>Sans Tor ni VPN, votre adresse IP sera visible par ces relais XFTP: %@.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
@@ -7360,11 +7579,6 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien
<target>Serveur XFTP</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You" xml:space="preserve">
<source>You</source>
<target>Vous</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
<source>You **must not** use the same database on two devices.</source>
<target>Vous **ne devez pas** utiliser la même base de données sur deux appareils.</target>
@@ -7714,11 +7928,19 @@ Répéter la demande de connexion ?</target>
<target>Votre base de données de chat n'est pas chiffrée - définisez une phrase secrète.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat preferences" xml:space="preserve">
<source>Your chat preferences</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>Vos profils de chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Votre contact a envoyé un fichier plus grand que la taille maximale supportée actuellement(%@).</target>
@@ -7734,6 +7956,10 @@ Répéter la demande de connexion ?</target>
<target>Vos contacts resteront connectés.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve">
<source>Your credentials may be sent unencrypted.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve">
<source>Your current chat database will be DELETED and REPLACED with the imported one.</source>
<target>Votre base de données de chat actuelle va être SUPPRIMEE et REMPLACEE par celle importée.</target>
@@ -7764,13 +7990,15 @@ Répéter la demande de connexion ?</target>
<target>Votre profil **%@** sera partagé.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
<target>Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts.
Les serveurs SimpleX ne peuvent pas voir votre profil.</target>
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
<target>Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts. Les serveurs SimpleX ne peuvent pas voir votre profil.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve">
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>Votre profil, vos contacts et les messages reçus sont stockés sur votre appareil.</target>
File diff suppressed because it is too large Load Diff
@@ -137,6 +137,11 @@
<target>%@ si vuole connettere!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<target>%1$@, %2$@</target>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ e %lld membri</target>
@@ -157,11 +162,36 @@
<target>%d giorni</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve">
<source>%d file(s) are still being downloaded.</source>
<target>%d file è/sono ancora in scaricamento.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) failed to download." xml:space="preserve">
<source>%d file(s) failed to download.</source>
<target>%d file ha/hanno fallito lo scaricamento.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were deleted." xml:space="preserve">
<source>%d file(s) were deleted.</source>
<target>%d file è/sono stato/i eliminato/i.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve">
<source>%d file(s) were not downloaded.</source>
<target>%d file non è/sono stato/i scaricato/i.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d hours" xml:space="preserve">
<source>%d hours</source>
<target>%d ore</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d messages not forwarded" xml:space="preserve">
<source>%d messages not forwarded</source>
<target>%d messaggi non inoltrati</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="%d min" xml:space="preserve">
<source>%d min</source>
<target>%d min</target>
@@ -723,7 +753,7 @@
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<target>Tutti gli profili</target>
<note>No comment provided by engineer.</note>
<note>profile dropdown</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source>
@@ -915,6 +945,11 @@
<target>Il codice di accesso dell'app viene sostituito da un codice di autodistruzione.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App session" xml:space="preserve">
<source>App session</source>
<target>Sessione dell'app</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App version" xml:space="preserve">
<source>App version</source>
<target>Versione dell'app</target>
@@ -1020,6 +1055,11 @@
<target>Auto-accetta immagini</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<target>Accetta automaticamente le impostazioni</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
<source>Back</source>
<target>Indietro</target>
@@ -1045,11 +1085,21 @@
<target>Hash del messaggio errato</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source>
<target>Chiamate migliorate</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<target>Gruppi migliorati</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source>
<target>Date dei messaggi migliorate.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>Messaggi migliorati</target>
@@ -1060,6 +1110,21 @@
<target>Rete migliorata</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source>
<target>Notifiche migliorate</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source>
<target>Sicurezza migliorata ✅</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source>
<target>Esperienza utente migliorata</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Black" xml:space="preserve">
<source>Black</source>
<target>Nero</target>
@@ -1193,7 +1258,7 @@
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Annulla</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve">
<source>Cancel migration</source>
@@ -1213,7 +1278,7 @@
<trans-unit id="Cannot receive file" xml:space="preserve">
<source>Cannot receive file</source>
<target>Impossibile ricevere il file</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
@@ -1341,6 +1406,16 @@
<target>Preferenze della chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences were changed." xml:space="preserve">
<source>Chat preferences were changed.</source>
<target>Le preferenze della chat sono state cambiate.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Chat profile" xml:space="preserve">
<source>Chat profile</source>
<target>Profilo utente</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat theme" xml:space="preserve">
<source>Chat theme</source>
<target>Tema della chat</target>
@@ -1617,7 +1692,7 @@ Questo è il tuo link una tantum!</target>
</trans-unit>
<trans-unit id="Connection and servers status." xml:space="preserve">
<source>Connection and servers status.</source>
<target>Stato di connessione e server.</target>
<target>Stato della connessione e dei server.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Connection error" xml:space="preserve">
@@ -1740,6 +1815,11 @@ Questo è il tuo link una tantum!</target>
<target>Versione core: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<target>Angolo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Correggere il nome a %@?</target>
@@ -1865,6 +1945,11 @@ Questo è il tuo link una tantum!</target>
<target>Tempo personalizzato</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source>
<target>Forma dei messaggi personalizzabile.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customize theme" xml:space="preserve">
<source>Customize theme</source>
<target>Personalizza il tema</target>
@@ -2159,6 +2244,11 @@ Questo è il tuo link una tantum!</target>
<target>Eliminare il database vecchio?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve">
<source>Delete pending connection?</source>
<target>Eliminare la connessione in attesa?</target>
@@ -2409,6 +2499,11 @@ Questo è il tuo link una tantum!</target>
<target>Non inviare la cronologia ai nuovi membri.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not use credentials with proxy." xml:space="preserve">
<source>Do not use credentials with proxy.</source>
<target>Non usare credenziali con proxy.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>Non creare un indirizzo</target>
@@ -2432,7 +2527,8 @@ Questo è il tuo link una tantum!</target>
<trans-unit id="Download" xml:space="preserve">
<source>Download</source>
<target>Scarica</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Download errors" xml:space="preserve">
<source>Download errors</source>
@@ -2449,6 +2545,11 @@ Questo è il tuo link una tantum!</target>
<target>Scarica file</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Download files" xml:space="preserve">
<source>Download files</source>
<target>Scarica i file</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Downloaded" xml:space="preserve">
<source>Downloaded</source>
<target>Scaricato</target>
@@ -2724,6 +2825,11 @@ Questo è il tuo link una tantum!</target>
<target>Errore nella modifica dell'indirizzo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<target>Errore nel cambio di profilo di connessione</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Errore nel cambio di ruolo</target>
@@ -2734,6 +2840,11 @@ Questo è il tuo link una tantum!</target>
<target>Errore nella modifica dell'impostazione</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<target>Errore nel passaggio a incognito!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Errore di connessione al server di inoltro %@. Riprova più tardi.</target>
@@ -2854,6 +2965,11 @@ Questo è il tuo link una tantum!</target>
<target>Errore nel caricamento dei server %@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error migrating settings" xml:space="preserve">
<source>Error migrating settings</source>
<target>Errore nella migrazione delle impostazioni</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<target>Errore di apertura della chat</target>
@@ -2862,7 +2978,7 @@ Questo è il tuo link una tantum!</target>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Errore nella ricezione del file</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Error reconnecting server" xml:space="preserve">
<source>Error reconnecting server</source>
@@ -2954,10 +3070,15 @@ Questo è il tuo link una tantum!</target>
<target>Errore nell'interruzione della chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<target>Errore nel cambio di profilo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Errore nel cambio di profilo!</target>
<note>No comment provided by engineer.</note>
<note>alertTitle</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
@@ -3002,8 +3123,7 @@ Questo è il tuo link una tantum!</target>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>Errore: %@</target>
<note>file error text
snd error text</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Error: URL is invalid" xml:space="preserve">
<source>Error: URL is invalid</source>
@@ -3090,6 +3210,13 @@ Questo è il tuo link una tantum!</target>
<target>Errore del file</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File errors:&#10;%@" xml:space="preserve">
<source>File errors:
%@</source>
<target>Errori di file:
%@</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve">
<source>File not found - most likely file was deleted or cancelled.</source>
<target>File non trovato - probabilmente è stato eliminato o annullato.</target>
@@ -3225,11 +3352,31 @@ Questo è il tuo link una tantum!</target>
<target>Inoltra</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Forward %d message(s)?" xml:space="preserve">
<source>Forward %d message(s)?</source>
<target>Inoltrare %d messaggio/i?</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Forward and save messages" xml:space="preserve">
<source>Forward and save messages</source>
<target>Inoltra e salva i messaggi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forward messages" xml:space="preserve">
<source>Forward messages</source>
<target>Inoltra i messaggi</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Forward messages without files?" xml:space="preserve">
<source>Forward messages without files?</source>
<target>Inoltrare i messaggi senza file?</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="Forwarded" xml:space="preserve">
<source>Forwarded</source>
<target>Inoltrato</target>
@@ -3240,6 +3387,11 @@ Questo è il tuo link una tantum!</target>
<target>Inoltrato da</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding %lld messages" xml:space="preserve">
<source>Forwarding %lld messages</source>
<target>Inoltro di %lld messaggi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve">
<source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source>
<target>Il server di inoltro %@ non è riuscito a connettersi al server di destinazione %@. Riprova più tardi.</target>
@@ -3289,11 +3441,6 @@ Errore: %2$@</target>
<target>Nome completo (facoltativo)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Full name:" xml:space="preserve">
<source>Full name:</source>
<target>Nome completo:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<target>Completamente decentralizzato: visibile solo ai membri.</target>
@@ -3539,6 +3686,11 @@ Errore: %2$@</target>
<target>Server ICE (uno per riga)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="IP address" xml:space="preserve">
<source>IP address</source>
<target>Indirizzo IP</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
<target>Se non potete incontrarvi di persona, mostra il codice QR in una videochiamata o condividi il link.</target>
@@ -3614,6 +3766,13 @@ Errore: %2$@</target>
<target>Importazione archivio</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage.
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>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<target>Consegna dei messaggi migliorata</target>
@@ -4184,6 +4343,11 @@ Questo è il tuo link per il gruppo %@!</target>
<target>Server dei messaggi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<target>Forma del messaggio</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>La fonte del messaggio resta privata.</target>
@@ -4234,6 +4398,11 @@ Questo è il tuo link per il gruppo %@!</target>
<target>Messaggi inviati</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages were deleted after you selected them." xml:space="preserve">
<source>Messages were deleted after you selected them.</source>
<target>I messaggi sono stati eliminati dopo che li hai selezionati.</target>
<note>alert message</note>
</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">
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
<target>I messaggi, i file e le chiamate sono protetti da **crittografia end-to-end** con perfect forward secrecy, ripudio e recupero da intrusione.</target>
@@ -4389,6 +4558,16 @@ Questo è il tuo link per il gruppo %@!</target>
<target>Nuovo codice di accesso</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve">
<source>New SOCKS credentials will be used every time you start the app.</source>
<target>Le nuove credenziali SOCKS verranno usate ogni volta che avvii l'app.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve">
<source>New SOCKS credentials will be used for each server.</source>
<target>Le nuove credenziali SOCKS verranno usate per ogni server.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<target>Nuova chat</target>
@@ -4509,6 +4688,16 @@ Questo è il tuo link per il gruppo %@!</target>
<target>Nessuna connessione di rete</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record speech" xml:space="preserve">
<source>No permission to record speech</source>
<target>Nessuna autorizzazione per registrare l'audio</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record video" xml:space="preserve">
<source>No permission to record video</source>
<target>Nessuna autorizzazione per registrare il video</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>Nessuna autorizzazione per registrare messaggi vocali</target>
@@ -4529,6 +4718,11 @@ Questo è il tuo link per il gruppo %@!</target>
<target>Nessuna selezione</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Nothing to forward!" xml:space="preserve">
<source>Nothing to forward!</source>
<target>Niente da inoltrare!</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Notifiche</target>
@@ -4561,7 +4755,7 @@ Questo è il tuo link per il gruppo %@!</target>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ok</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Old database" xml:space="preserve">
<source>Old database</source>
@@ -4752,6 +4946,13 @@ Richiede l'attivazione della VPN.</target>
<target>Altri %@ server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other file errors:&#10;%@" xml:space="preserve">
<source>Other file errors:
%@</source>
<target>Altri errori di file:
%@</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>Conteggio PING</target>
@@ -4787,6 +4988,11 @@ Richiede l'attivazione della VPN.</target>
<target>Codice di accesso impostato!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password" xml:space="preserve">
<source>Password</source>
<target>Password</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password to show" xml:space="preserve">
<source>Password to show</source>
<target>Password per mostrare</target>
@@ -4936,6 +5142,11 @@ Errore: %@</target>
<target>Interfaccia polacca</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Port" xml:space="preserve">
<source>Port</source>
<target>Porta</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve">
<source>Possibly, certificate fingerprint in server address is incorrect</source>
<target>Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata</target>
@@ -5021,16 +5232,6 @@ Errore: %@</target>
<target>Immagini del profilo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<target>Nome del profilo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<target>Nome del profilo:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source>
<target>Password del profilo</target>
@@ -5133,6 +5334,11 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Server via proxy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Proxy requires password" xml:space="preserve">
<source>Proxy requires password</source>
<target>Il proxy richiede una password</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>Notifiche push</target>
@@ -5354,6 +5560,11 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Rimuovi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
<target>Rimuovere l'archivio?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove image" xml:space="preserve">
<source>Remove image</source>
<target>Rimuovi immagine</target>
@@ -5534,6 +5745,11 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Server SMP</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SOCKS proxy" xml:space="preserve">
<source>SOCKS proxy</source>
<target>Proxy SOCKS</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Safely receive files" xml:space="preserve">
<source>Safely receive files</source>
<target>Ricevi i file in sicurezza</target>
@@ -5547,12 +5763,13 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<target>Salva</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
<source>Save (and notify contacts)</source>
<target>Salva (e avvisa i contatti)</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Save and notify contact" xml:space="preserve">
<source>Save and notify contact</source>
@@ -5579,11 +5796,6 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Salva archivio</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save auto-accept settings" xml:space="preserve">
<source>Save auto-accept settings</source>
<target>Salva le impostazioni di accettazione automatica</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save group profile" xml:space="preserve">
<source>Save group profile</source>
<target>Salva il profilo del gruppo</target>
@@ -5619,16 +5831,16 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Salvare i server?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save settings?" xml:space="preserve">
<source>Save settings?</source>
<target>Salvare le impostazioni?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save welcome message?" xml:space="preserve">
<source>Save welcome message?</source>
<target>Salvare il messaggio di benvenuto?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save your profile?" xml:space="preserve">
<source>Save your profile?</source>
<target>Salvare il profilo?</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Saved" xml:space="preserve">
<source>Saved</source>
<target>Salvato</target>
@@ -5649,6 +5861,11 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Messaggio salvato</target>
<note>message info title</note>
</trans-unit>
<trans-unit id="Saving %lld messages" xml:space="preserve">
<source>Saving %lld messages</source>
<target>Salvataggio di %lld messaggi</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scale" xml:space="preserve">
<source>Scale</source>
<target>Scala</target>
@@ -5729,6 +5946,11 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Seleziona</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<target>Seleziona il profilo di chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>%lld selezionato</target>
@@ -5847,7 +6069,7 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>Il mittente ha annullato il trasferimento del file.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Sender may have deleted the connection request." xml:space="preserve">
<source>Sender may have deleted the connection request.</source>
@@ -5944,6 +6166,11 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Inviato via proxy</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<target>Server</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server address" xml:space="preserve">
<source>Server address</source>
<target>Indirizzo server</target>
@@ -6064,6 +6291,11 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Impostazioni</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Settings were changed." xml:space="preserve">
<source>Settings were changed.</source>
<target>Le impostazioni sono state cambiate.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Shape profile images" xml:space="preserve">
<source>Shape profile images</source>
<target>Forma delle immagini del profilo</target>
@@ -6099,6 +6331,11 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Condividi link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<target>Condividi il profilo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Condividi questo link di invito una tantum</target>
@@ -6234,6 +6471,11 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Invito SimpleX una tantum</target>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Modalità incognito semplificata</target>
@@ -6264,6 +6506,11 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Leggera</target>
<note>blur media</note>
</trans-unit>
<trans-unit id="Some app settings were not migrated." xml:space="preserve">
<source>Some app settings were not migrated.</source>
<target>Alcune impostazioni dell'app non sono state migrate.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some file(s) were not exported:" xml:space="preserve">
<source>Some file(s) were not exported:</source>
<target>Alcuni file non sono stati esportati:</target>
@@ -6404,6 +6651,16 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>Supporta SimpleX Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<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>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>Sistema</target>
@@ -6439,6 +6696,11 @@ Attivalo nelle impostazioni *Rete e server*.</target>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<target>Coda</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Scatta foto</target>
@@ -6631,6 +6893,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta
<target>Il testo che hai incollato non è un link SimpleX.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve">
<source>The uploaded database archive will be permanently removed from the servers.</source>
<target>L'archivio del database caricato verrà rimosso definitivamente dai server.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Themes" xml:space="preserve">
<source>Themes</source>
<target>Temi</target>
@@ -6758,6 +7025,16 @@ You will be prompted to complete authentication before this feature is enabled.<
Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzionalità.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve">
<source>To record speech please grant permission to use Microphone.</source>
<target>Per registrare l'audio, concedi l'autorizzazione di usare il microfono.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record video please grant permission to use Camera." xml:space="preserve">
<source>To record video please grant permission to use Camera.</source>
<target>Per registrare il video, concedi l'autorizzazione di usare la fotocamera.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve">
<source>To record voice message please grant permission to use Microphone.</source>
<target>Per registrare un messaggio vocale, concedi l'autorizzazione all'uso del microfono.</target>
@@ -6911,7 +7188,7 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio
<trans-unit id="Unknown servers!" xml:space="preserve">
<source>Unknown servers!</source>
<target>Server sconosciuti!</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
@@ -7025,6 +7302,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e
<target>Usa gli host .onion</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SOCKS proxy" xml:space="preserve">
<source>Use SOCKS proxy</source>
<target>Usa proxy SOCKS</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
<source>Use SimpleX Chat servers?</source>
<target>Usare i server di SimpleX Chat?</target>
@@ -7090,16 +7372,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e
<target>Usa l'app con una mano sola.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User profile" xml:space="preserve">
<source>User profile</source>
<target>Profilo utente</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User selection" xml:space="preserve">
<source>User selection</source>
<target>Selezione utente</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<target>Nome utente</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
<source>Using SimpleX Chat servers.</source>
<target>Utilizzo dei server SimpleX Chat.</target>
@@ -7333,7 +7615,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
<target>Senza Tor o VPN, il tuo indirizzo IP sarà visibile a questi relay XFTP: %@.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
@@ -7360,11 +7642,6 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e
<target>Server XFTP</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You" xml:space="preserve">
<source>You</source>
<target>Tu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
<source>You **must not** use the same database on two devices.</source>
<target>**Non devi** usare lo stesso database su due dispositivi.</target>
@@ -7714,11 +7991,21 @@ Ripetere la richiesta di connessione?</target>
<target>Il tuo database della chat non è crittografato: imposta la password per crittografarlo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat preferences" xml:space="preserve">
<source>Your chat preferences</source>
<target>Le tue preferenze della chat</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>I tuoi profili di chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<target>La tua connessione è stata spostata a %@, ma si è verificato un errore imprevisto durante il reindirizzamento al profilo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Il tuo contatto ha inviato un file più grande della dimensione massima attualmente supportata (%@).</target>
@@ -7734,6 +8021,11 @@ Ripetere la richiesta di connessione?</target>
<target>I tuoi contatti resteranno connessi.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve">
<source>Your credentials may be sent unencrypted.</source>
<target>Le credenziali potrebbero essere inviate in chiaro.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve">
<source>Your current chat database will be DELETED and REPLACED with the imported one.</source>
<target>Il tuo attuale database della chat verrà ELIMINATO e SOSTITUITO con quello importato.</target>
@@ -7764,13 +8056,16 @@ Ripetere la richiesta di connessione?</target>
<target>Verrà condiviso il tuo profilo **%@**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
<target>Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti.
I server di SimpleX non possono vedere il tuo profilo.</target>
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
<target>Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. I server di SimpleX non possono vedere il tuo profilo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve">
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
<target>Il tuo profilo è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato a tutti i tuoi contatti.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo.</target>
@@ -7948,7 +8243,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
</trans-unit>
<trans-unit id="changed your role to %@" xml:space="preserve">
<source>changed your role to %@</source>
<target>cambiato il tuo ruolo in %@</target>
<target>ha cambiato il tuo ruolo in %@</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="changing address for %@…" xml:space="preserve">
@@ -137,6 +137,10 @@
<target>%@ が接続を希望しています!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@や%@など%lld人のメンバー</target>
@@ -157,11 +161,31 @@
<target>%d 日</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve">
<source>%d file(s) are still being downloaded.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) failed to download." xml:space="preserve">
<source>%d file(s) failed to download.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were deleted." xml:space="preserve">
<source>%d file(s) were deleted.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve">
<source>%d file(s) were not downloaded.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d hours" xml:space="preserve">
<source>%d hours</source>
<target>%d 時</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d messages not forwarded" xml:space="preserve">
<source>%d messages not forwarded</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="%d min" xml:space="preserve">
<source>%d min</source>
<target>%d 分</target>
@@ -703,7 +727,7 @@
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<note>No comment provided by engineer.</note>
<note>profile dropdown</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source>
@@ -891,6 +915,10 @@
<target>アプリのパスコードは自己破壊パスコードに置き換えられます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App session" xml:space="preserve">
<source>App session</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App version" xml:space="preserve">
<source>App version</source>
<target>アプリのバージョン</target>
@@ -990,6 +1018,10 @@
<target>画像を自動的に受信</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
<source>Back</source>
<target>戻る</target>
@@ -1013,10 +1045,18 @@
<target>メッセージのハッシュ値問題</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>より良いメッセージ</target>
@@ -1026,6 +1066,18 @@
<source>Better networking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Black" xml:space="preserve">
<source>Black</source>
<note>No comment provided by engineer.</note>
@@ -1144,7 +1196,7 @@
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>中止</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve">
<source>Cancel migration</source>
@@ -1162,7 +1214,7 @@
<trans-unit id="Cannot receive file" xml:space="preserve">
<source>Cannot receive file</source>
<target>ファイル受信ができません</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
@@ -1283,6 +1335,15 @@
<target>チャット設定</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences were changed." xml:space="preserve">
<source>Chat preferences were changed.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Chat profile" xml:space="preserve">
<source>Chat profile</source>
<target>ユーザープロフィール</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat theme" xml:space="preserve">
<source>Chat theme</source>
<note>No comment provided by engineer.</note>
@@ -1640,6 +1701,10 @@ This is your own one-time link!</source>
<target>コアのバージョン: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
@@ -1755,6 +1820,10 @@ This is your own one-time link!</source>
<target>カスタム時間</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customize theme" xml:space="preserve">
<source>Customize theme</source>
<note>No comment provided by engineer.</note>
@@ -2041,6 +2110,10 @@ This is your own one-time link!</source>
<target>古いデータベースを削除しますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve">
<source>Delete pending connection?</source>
<target>接続待ちの接続を削除しますか?</target>
@@ -2272,6 +2345,10 @@ This is your own one-time link!</source>
<source>Do not send history to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not use credentials with proxy." xml:space="preserve">
<source>Do not use credentials with proxy.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>アドレスを作成しないでください</target>
@@ -2294,7 +2371,8 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Download" xml:space="preserve">
<source>Download</source>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Download errors" xml:space="preserve">
<source>Download errors</source>
@@ -2309,6 +2387,10 @@ This is your own one-time link!</source>
<target>ファイルをダウンロード</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Download files" xml:space="preserve">
<source>Download files</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Downloaded" xml:space="preserve">
<source>Downloaded</source>
<note>No comment provided by engineer.</note>
@@ -2569,6 +2651,10 @@ This is your own one-time link!</source>
<target>アドレス変更にエラー発生</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>役割変更にエラー発生</target>
@@ -2579,6 +2665,10 @@ This is your own one-time link!</source>
<target>設定変更にエラー発生</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -2694,6 +2784,10 @@ This is your own one-time link!</source>
<target>%@ サーバーのロード中にエラーが発生</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error migrating settings" xml:space="preserve">
<source>Error migrating settings</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
@@ -2701,7 +2795,7 @@ This is your own one-time link!</source>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>ファイル受信にエラー発生</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Error reconnecting server" xml:space="preserve">
<source>Error reconnecting server</source>
@@ -2787,10 +2881,14 @@ This is your own one-time link!</source>
<target>チャット停止にエラー発生</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>プロフィール切り替えにエラー発生!</target>
<note>No comment provided by engineer.</note>
<note>alertTitle</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
@@ -2833,8 +2931,7 @@ This is your own one-time link!</source>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>エラー : %@</target>
<note>file error text
snd error text</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Error: URL is invalid" xml:space="preserve">
<source>Error: URL is invalid</source>
@@ -2915,6 +3012,11 @@ This is your own one-time link!</source>
<source>File error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File errors:&#10;%@" xml:space="preserve">
<source>File errors:
%@</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve">
<source>File not found - most likely file was deleted or cancelled.</source>
<note>file error text</note>
@@ -3041,10 +3143,26 @@ This is your own one-time link!</source>
<source>Forward</source>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Forward %d message(s)?" xml:space="preserve">
<source>Forward %d message(s)?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Forward and save messages" xml:space="preserve">
<source>Forward and save messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forward messages" xml:space="preserve">
<source>Forward messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Forward messages without files?" xml:space="preserve">
<source>Forward messages without files?</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarded" xml:space="preserve">
<source>Forwarded</source>
<note>No comment provided by engineer.</note>
@@ -3053,6 +3171,10 @@ This is your own one-time link!</source>
<source>Forwarded from</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding %lld messages" xml:space="preserve">
<source>Forwarding %lld messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve">
<source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -3094,11 +3216,6 @@ Error: %2$@</source>
<target>フルネーム (任意)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Full name:" xml:space="preserve">
<source>Full name:</source>
<target>フルネーム:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<note>No comment provided by engineer.</note>
@@ -3336,6 +3453,10 @@ Error: %2$@</source>
<target>ICEサーバ (1行に1サーバ)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="IP address" xml:space="preserve">
<source>IP address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
<target>直接会えない場合は、ビデオ通話で QR コードを表示するか、リンクを共有してください。</target>
@@ -3408,6 +3529,11 @@ Error: %2$@</source>
<source>Importing archive</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<note>No comment provided by engineer.</note>
@@ -3942,6 +4068,10 @@ This is your link for group %@!</source>
<source>Message servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<note>No comment provided by engineer.</note>
@@ -3985,12 +4115,18 @@ This is your link for group %@!</source>
<source>Messages sent</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages were deleted after you selected them." xml:space="preserve">
<source>Messages were deleted after you selected them.</source>
<note>alert message</note>
</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">
<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>
</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">
<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>
</trans-unit>
<trans-unit id="Migrate device" xml:space="preserve">
@@ -4127,6 +4263,14 @@ This is your link for group %@!</source>
<target>新しいパスコード</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve">
<source>New SOCKS credentials will be used every time you start the app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve">
<source>New SOCKS credentials will be used for each server.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
@@ -4241,6 +4385,14 @@ This is your link for group %@!</source>
<source>No network connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record speech" xml:space="preserve">
<source>No permission to record speech</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record video" xml:space="preserve">
<source>No permission to record video</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>音声メッセージを録音する権限がありません</target>
@@ -4259,6 +4411,10 @@ This is your link for group %@!</source>
<source>Nothing selected</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Nothing to forward!" xml:space="preserve">
<source>Nothing to forward!</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>通知</target>
@@ -4290,7 +4446,7 @@ This is your link for group %@!</source>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>OK</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Old database" xml:space="preserve">
<source>Old database</source>
@@ -4470,6 +4626,11 @@ VPN を有効にする必要があります。</target>
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other file errors:&#10;%@" xml:space="preserve">
<source>Other file errors:
%@</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>PING回数</target>
@@ -4505,6 +4666,10 @@ VPN を有効にする必要があります。</target>
<target>パスコードを設定しました!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password" xml:space="preserve">
<source>Password</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password to show" xml:space="preserve">
<source>Password to show</source>
<target>パスワードを表示する</target>
@@ -4641,6 +4806,10 @@ Error: %@</source>
<target>ポーランド語UI</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Port" xml:space="preserve">
<source>Port</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve">
<source>Possibly, certificate fingerprint in server address is incorrect</source>
<target>サーバアドレスの証明証IDが正しくないかもしれません</target>
@@ -4719,14 +4888,6 @@ Error: %@</source>
<source>Profile images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source>
<target>プロフィールのパスワード</target>
@@ -4822,6 +4983,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Proxied servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Proxy requires password" xml:space="preserve">
<source>Proxy requires password</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>プッシュ通知</target>
@@ -5027,6 +5192,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>削除</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove image" xml:space="preserve">
<source>Remove image</source>
<note>No comment provided by engineer.</note>
@@ -5194,6 +5363,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>SMP server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SOCKS proxy" xml:space="preserve">
<source>SOCKS proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Safely receive files" xml:space="preserve">
<source>Safely receive files</source>
<note>No comment provided by engineer.</note>
@@ -5205,12 +5378,13 @@ Enable in *Network &amp; servers* settings.</source>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<target>保存</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
<source>Save (and notify contacts)</source>
<target>保存(連絡先に通知)</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Save and notify contact" xml:space="preserve">
<source>Save and notify contact</source>
@@ -5236,11 +5410,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>アーカイブを保存</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save auto-accept settings" xml:space="preserve">
<source>Save auto-accept settings</source>
<target>自動受け入れ設定を保存する</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save group profile" xml:space="preserve">
<source>Save group profile</source>
<target>グループプロフィールの保存</target>
@@ -5276,16 +5445,15 @@ Enable in *Network &amp; servers* settings.</source>
<target>サーバを保存しますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save settings?" xml:space="preserve">
<source>Save settings?</source>
<target>設定を保存しますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save welcome message?" xml:space="preserve">
<source>Save welcome message?</source>
<target>ウェルカムメッセージを保存しますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save your profile?" xml:space="preserve">
<source>Save your profile?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Saved" xml:space="preserve">
<source>Saved</source>
<note>No comment provided by engineer.</note>
@@ -5303,6 +5471,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Saved message</source>
<note>message info title</note>
</trans-unit>
<trans-unit id="Saving %lld messages" xml:space="preserve">
<source>Saving %lld messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scale" xml:space="preserve">
<source>Scale</source>
<note>No comment provided by engineer.</note>
@@ -5376,6 +5548,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>選択</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<note>No comment provided by engineer.</note>
@@ -5485,7 +5661,7 @@ Enable in *Network &amp; servers* settings.</source>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>送信者がファイル転送をキャンセルしました。</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Sender may have deleted the connection request." xml:space="preserve">
<source>Sender may have deleted the connection request.</source>
@@ -5571,6 +5747,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Sent via proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server address" xml:space="preserve">
<source>Server address</source>
<note>No comment provided by engineer.</note>
@@ -5680,6 +5860,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>設定</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Settings were changed." xml:space="preserve">
<source>Settings were changed.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Shape profile images" xml:space="preserve">
<source>Shape profile images</source>
<note>No comment provided by engineer.</note>
@@ -5713,6 +5897,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>リンクを送る</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<note>No comment provided by engineer.</note>
@@ -5839,6 +6027,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>SimpleX使い捨て招待リンク</target>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>シークレットモードの簡素化</target>
@@ -5867,6 +6059,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Soft</source>
<note>blur media</note>
</trans-unit>
<trans-unit id="Some app settings were not migrated." xml:space="preserve">
<source>Some app settings were not migrated.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some file(s) were not exported:" xml:space="preserve">
<source>Some file(s) were not exported:</source>
<note>No comment provided by engineer.</note>
@@ -5995,6 +6191,14 @@ Enable in *Network &amp; servers* settings.</source>
<target>Simplex Chatを支援</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>システム</target>
@@ -6029,6 +6233,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>写真を撮影</target>
@@ -6212,6 +6420,10 @@ It can happen because of some bug or when the connection is compromised.</source
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve">
<source>The uploaded database archive will be permanently removed from the servers.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Themes" xml:space="preserve">
<source>Themes</source>
<note>No comment provided by engineer.</note>
@@ -6327,6 +6539,14 @@ You will be prompted to complete authentication before this feature is enabled.<
オンにするには、認証ステップが行われます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve">
<source>To record speech please grant permission to use Microphone.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record video please grant permission to use Camera." xml:space="preserve">
<source>To record video please grant permission to use Camera.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve">
<source>To record voice message please grant permission to use Microphone.</source>
<target>音声メッセージを録音する場合は、マイクの使用を許可してください。</target>
@@ -6468,7 +6688,7 @@ You will be prompted to complete authentication before this feature is enabled.<
</trans-unit>
<trans-unit id="Unknown servers!" xml:space="preserve">
<source>Unknown servers!</source>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
@@ -6573,6 +6793,10 @@ To connect, please ask your contact to create another connection link and check
<target>.onionホストを使う</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SOCKS proxy" xml:space="preserve">
<source>Use SOCKS proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
<source>Use SimpleX Chat servers?</source>
<target>SimpleX チャット サーバーを使用しますか?</target>
@@ -6632,15 +6856,14 @@ To connect, please ask your contact to create another connection link and check
<source>Use the app with one hand.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User profile" xml:space="preserve">
<source>User profile</source>
<target>ユーザープロフィール</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User selection" xml:space="preserve">
<source>User selection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
<source>Using SimpleX Chat servers.</source>
<target>SimpleX チャット サーバーを使用する。</target>
@@ -6853,7 +7076,7 @@ To connect, please ask your contact to create another connection link and check
</trans-unit>
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
@@ -6877,11 +7100,6 @@ To connect, please ask your contact to create another connection link and check
<source>XFTP server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You" xml:space="preserve">
<source>You</source>
<target>あなた</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
<source>You **must not** use the same database on two devices.</source>
<note>No comment provided by engineer.</note>
@@ -7207,11 +7425,19 @@ Repeat connection request?</source>
<target>チャット データベースは暗号化されていません - 暗号化するにはパスフレーズを設定してください。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat preferences" xml:space="preserve">
<source>Your chat preferences</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>あなたのチャットプロフィール</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>連絡先が現在サポートされている最大サイズ (%@) より大きいファイルを送信しました。</target>
@@ -7227,6 +7453,10 @@ Repeat connection request?</source>
<target>連絡先は接続されたままになります。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve">
<source>Your credentials may be sent unencrypted.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve">
<source>Your current chat database will be DELETED and REPLACED with the imported one.</source>
<target>現在のチャット データベースは削除され、インポートされたデータベースに置き換えられます。</target>
@@ -7256,13 +7486,15 @@ Repeat connection request?</source>
<target>あなたのプロファイル **%@** が共有されます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
<target>プロフィールはデバイスに保存され、連絡先とのみ共有されます。
SimpleX サーバーはあなたのプロファイルを参照できません。</target>
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
<target>プロフィールはデバイスに保存され、連絡先とのみ共有されます。 SimpleX サーバーはあなたのプロファイルを参照できません。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve">
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。</target>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -5234,6 +5234,342 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi
<target state="translated">%1$@ em %2$@:</target>
<note>copied message info, &lt;sender&gt; at &lt;time&gt;</note>
</trans-unit>
<trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
<source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source>
<target state="translated">Permitir que seus contatos deletem mensagens enviadas de maneira irreversível. (24 horas)</target>
</trans-unit>
<trans-unit id="%@ downloaded" xml:space="preserve" approved="no">
<source>%@ downloaded</source>
<target state="translated">baixado</target>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve" approved="no">
<source>%@ uploaded</source>
<target state="translated">transferido</target>
</trans-unit>
<trans-unit id="A new random profile will be shared." xml:space="preserve" approved="no">
<source>A new random profile will be shared.</source>
<target state="translated">Um novo perfil aleatório será compartilhado.</target>
</trans-unit>
<trans-unit id="Camera not available" xml:space="preserve" approved="no">
<source>Camera not available</source>
<target state="translated">Câmera indisponível</target>
</trans-unit>
<trans-unit id="Admins can block a member for all." xml:space="preserve" approved="no">
<source>Admins can block a member for all.</source>
<target state="translated">Administradores podem bloquear um membro para todos.</target>
</trans-unit>
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
<target state="translated">Permitir que mensagens enviadas sejam deletadas de maneira irreversível. (24 horas)</target>
</trans-unit>
<trans-unit id="Apply" xml:space="preserve" approved="no">
<source>Apply</source>
<target state="translated">Aplicar</target>
</trans-unit>
<trans-unit id="Accent" xml:space="preserve" approved="no">
<source>Accent</source>
<target state="translated">Esquema</target>
</trans-unit>
<trans-unit id="Accept connection request?" xml:space="preserve" approved="no">
<source>Accept connection request?</source>
<target state="translated">Aceitar solicitação de conexão?</target>
</trans-unit>
<trans-unit id="Active connections" xml:space="preserve" approved="no">
<source>Active connections</source>
<target state="translated">Conexões ativas</target>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve" approved="no">
<source>Add contact</source>
<target state="translated">Adicionar contato</target>
</trans-unit>
<trans-unit id="Additional accent" xml:space="preserve" approved="no">
<source>Additional accent</source>
<target state="translated">Esquema adicional</target>
</trans-unit>
<trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve" approved="no">
<source>All new messages from %@ will be hidden!</source>
<target state="translated">Todas as novas mensagens de %@ serão ocultas!</target>
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve" approved="no">
<source>All profiles</source>
<target state="translated">Todos perfis</target>
</trans-unit>
<trans-unit id="Allow calls?" xml:space="preserve" approved="no">
<source>Allow calls?</source>
<target state="translated">Permitir chamadas?</target>
</trans-unit>
<trans-unit id="Archive contacts to chat later." xml:space="preserve" approved="no">
<source>Archive contacts to chat later.</source>
<target state="translated">Arquivar contatos para conversar depois.</target>
</trans-unit>
<trans-unit id="Blur media" xml:space="preserve" approved="no">
<source>Blur media</source>
<target state="translated">Censurar mídia</target>
</trans-unit>
<trans-unit id="Calls prohibited!" xml:space="preserve" approved="no">
<source>Calls prohibited!</source>
<target state="translated">Chamadas proibidas!</target>
</trans-unit>
<trans-unit id="Can't call contact" xml:space="preserve" approved="no">
<source>Can't call contact</source>
<target state="translated">Não foi possível ligar para o contato</target>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve" approved="no">
<source>%lld messages marked deleted</source>
<target state="translated">mensagens deletadas</target>
</trans-unit>
<trans-unit id="0 sec" xml:space="preserve" approved="no">
<source>0 sec</source>
<target state="translated">0 seg</target>
</trans-unit>
<trans-unit id="%lld messages blocked" xml:space="preserve" approved="no">
<source>%lld messages blocked</source>
<target state="translated">mensagens bloqueadas</target>
</trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve" approved="no">
<source>%lld messages blocked by admin</source>
<target state="translated">mensagens bloqueadas pelo administrador</target>
</trans-unit>
<trans-unit id="**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." xml:space="preserve" approved="no">
<source>**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.</source>
<target state="translated">**Nota**: usar o mesmo banco de dados em dois dispositivos irá quebrar a desencriptação das mensagens de suas conexões como uma medida de segurança.</target>
</trans-unit>
<trans-unit id="- more stable message delivery.&#10;- a bit better groups.&#10;- and more!" xml:space="preserve" approved="no">
<source>- more stable message delivery.
- a bit better groups.
- and more!</source>
<target state="translated">- entrega de mensagens mais estável.
- grupos melhorados.
- e muito mais!</target>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no">
<source>All messages will be deleted - this cannot be undone!</source>
<target state="translated">Todas as mensagens serão deletadas - isto não pode ser desfeito!</target>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve" approved="no">
<source>Allow to send files and media.</source>
<target state="translated">Permitir o envio de arquivos e mídia.</target>
</trans-unit>
<trans-unit id="Allow to send SimpleX links." xml:space="preserve" approved="no">
<source>Allow to send SimpleX links.</source>
<target state="translated">Permitir envio de links SimpleX.</target>
</trans-unit>
<trans-unit id="Block for all" xml:space="preserve" approved="no">
<source>Block for all</source>
<target state="translated">Bloquear para todos</target>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve" approved="no">
<source>Block member</source>
<target state="translated">Bloquear membro</target>
</trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve" approved="no">
<source>Blocked by admin</source>
<target state="translated">Bloqueado por um administrador</target>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve" approved="no">
<source>Block group members</source>
<target state="translated">Bloquear membros de grupo</target>
</trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve" approved="no">
<source>Block member for all?</source>
<target state="translated">Bloquear membro para todos?</target>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve" approved="no">
<source>Block member?</source>
<target state="translated">Bloquear membro?</target>
</trans-unit>
<trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
<source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source>
<target state="translated">Você e seu contato podem apagar mensagens enviadas de maneira irreversível. (24 horas)</target>
</trans-unit>
<trans-unit id="Can't call member" xml:space="preserve" approved="no">
<source>Can't call member</source>
<target state="translated">Não foi possível ligar para este membro</target>
</trans-unit>
<trans-unit id="Can't message member" xml:space="preserve" approved="no">
<source>Can't message member</source>
<target state="translated">Não foi possível enviar mensagem para este membro</target>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve" approved="no">
<source>Cancel migration</source>
<target state="translated">Cancelar migração</target>
</trans-unit>
<trans-unit id="Abort" xml:space="preserve" approved="no">
<source>Abort</source>
<target state="translated">Abortar</target>
</trans-unit>
<trans-unit id="Abort changing address" xml:space="preserve" approved="no">
<source>Abort changing address</source>
<target state="translated">Abortar troca de endereço</target>
</trans-unit>
<trans-unit id="Abort changing address?" xml:space="preserve" approved="no">
<source>Abort changing address?</source>
<target state="translated">Abortar troca de endereço?</target>
</trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve" approved="no">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<target state="translated">- notificar contatos apagados de maneira opcional.
- nome de perfil com espaços.
- e muito mais!</target>
</trans-unit>
<trans-unit id="Allow sharing" xml:space="preserve" approved="no">
<source>Allow sharing</source>
<target state="translated">Permitir compartilhamento</target>
</trans-unit>
<trans-unit id="Block" xml:space="preserve" approved="no">
<source>Block</source>
<target state="translated">Bloquear</target>
</trans-unit>
<trans-unit id="Additional accent 2" xml:space="preserve" approved="no">
<source>Additional accent 2</source>
<target state="translated">Esquema adicional 2</target>
</trans-unit>
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve" approved="no">
<source>Address change will be aborted. Old receiving address will be used.</source>
<target state="translated">Alteração de endereço será abortada. O endereço antigo será utilizado.</target>
</trans-unit>
<trans-unit id="Advanced settings" xml:space="preserve" approved="no">
<source>Advanced settings</source>
<target state="translated">Configurações avançadas</target>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve" approved="no">
<source>All data is private to your device.</source>
<target state="translated">Toda informação é privada em seu dispositivo.</target>
</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" approved="no">
<source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source>
<target state="translated">Todos os seus contatos, conversas e arquivos serão encriptados e enviados em pedaços para nós XFTP.</target>
</trans-unit>
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve" approved="no">
<source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source>
<target state="translated">Permitir deletar mensagens de maneira irreversível apenas se seu contato permitir para você. (24 horas)</target>
</trans-unit>
<trans-unit id="Already connecting!" xml:space="preserve" approved="no">
<source>Already connecting!</source>
<target state="translated">Já está conectando!</target>
</trans-unit>
<trans-unit id="Already joining the group!" xml:space="preserve" approved="no">
<source>Already joining the group!</source>
<target state="translated">Já está entrando no grupo!</target>
</trans-unit>
<trans-unit id="Always use private routing." xml:space="preserve" approved="no">
<source>Always use private routing.</source>
<target state="translated">Sempre use rotas privadas.</target>
</trans-unit>
<trans-unit id="Apply to" xml:space="preserve" approved="no">
<source>Apply to</source>
<target state="translated">Aplicar em</target>
</trans-unit>
<trans-unit id="Archiving database" xml:space="preserve" approved="no">
<source>Archiving database</source>
<target state="translated">Arquivando banco de dados</target>
</trans-unit>
<trans-unit id="Black" xml:space="preserve" approved="no">
<source>Black</source>
<target state="translated">Preto</target>
</trans-unit>
<trans-unit id="Cannot forward message" xml:space="preserve" approved="no">
<source>Cannot forward message</source>
<target state="translated">Não é possível encaminhar mensagem</target>
</trans-unit>
<trans-unit id="(new)" xml:space="preserve" approved="no">
<source>(new)</source>
<target state="translated">(novo)</target>
</trans-unit>
<trans-unit id="(this device v%@)" xml:space="preserve" approved="no">
<source>(this device v%@)</source>
<target state="translated">este dispositivo</target>
</trans-unit>
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve" approved="no">
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
<target state="translated">**Adicionar contato**: criar um novo link de convite ou conectar via um link que você recebeu.</target>
</trans-unit>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no">
<source>**Create group**: to create a new group.</source>
<target state="translated">**Criar grupo**: criar um novo grupo.</target>
</trans-unit>
<trans-unit id="**Warning**: the archive will be removed." xml:space="preserve" approved="no">
<source>**Warning**: the archive will be removed.</source>
<target state="translated">**Aviso**: o arquivo será removido.</target>
</trans-unit>
<trans-unit id="A few more things" xml:space="preserve" approved="no">
<source>A few more things</source>
<target state="translated">E mais algumas coisas</target>
</trans-unit>
<trans-unit id="Archived contacts" xml:space="preserve" approved="no">
<source>Archived contacts</source>
<target state="translated">Contatos arquivados</target>
</trans-unit>
<trans-unit id="Cellular" xml:space="preserve" approved="no">
<source>Cellular</source>
<target state="translated">Rede móvel</target>
</trans-unit>
<trans-unit id="%d file(s) failed to download." xml:space="preserve" approved="no">
<source>%d file(s) failed to download.</source>
<target state="translated">%d arquivo(s) falharam ao ser baixados.</target>
</trans-unit>
<trans-unit id="%d file(s) were deleted." xml:space="preserve" approved="no">
<source>%d file(s) were deleted.</source>
<target state="translated">%d arquivo(s) foram apagados.</target>
</trans-unit>
<trans-unit id="%d messages not forwarded" xml:space="preserve" approved="no">
<source>%d messages not forwarded</source>
<target state="translated">%d mensagens não encaminhadas</target>
</trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve" approved="no">
<source>Bad desktop address</source>
<target state="translated">Endereço de desktop incorreto</target>
</trans-unit>
<trans-unit id="Blur for better privacy." xml:space="preserve" approved="no">
<source>Blur for better privacy.</source>
<target state="translated">Borrar para melhor privacidade.</target>
</trans-unit>
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve" approved="no">
<source>%d file(s) are still being downloaded.</source>
<target state="translated">%d arquivo(s) ainda estão sendo baixados.</target>
</trans-unit>
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve" approved="no">
<source>%d file(s) were not downloaded.</source>
<target state="translated">%d arquivo(s) não foram baixados.</target>
</trans-unit>
<trans-unit id="Chat colors" xml:space="preserve" approved="no">
<source>Chat colors</source>
<target state="translated">Cores do chat</target>
</trans-unit>
<trans-unit id="%lld group events" xml:space="preserve" approved="no">
<source>%lld group events</source>
<target state="translated">%lld eventos do grupo</target>
</trans-unit>
<trans-unit id="%lld messages moderated by %@" xml:space="preserve" approved="no">
<source>%lld messages moderated by %@</source>
<target state="translated">%lld mensagens moderadas por %@</target>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve" approved="no">
<source>%1$@, %2$@</source>
<target state="translated">%1$@, %2$@</target>
</trans-unit>
<trans-unit id="%lld new interface languages" xml:space="preserve" approved="no">
<source>%lld new interface languages</source>
<target state="translated">%lld novos idiomas de interface</target>
</trans-unit>
<trans-unit id="Better networking" xml:space="preserve" approved="no">
<source>Better networking</source>
<target state="translated">Melhores redes</target>
</trans-unit>
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve" approved="no">
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
<target state="translated">Búlgaro, Finlandês, Tailandês e Ucraniano - obrigado aos usuários e [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve" approved="no">
<source>Better groups</source>
<target state="translated">Melhores grupos</target>
</trans-unit>
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve" approved="no">
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
<target state="translated">Capacidade excedida - o destinatário não recebeu as mensagens enviadas anteriormente.</target>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="pt-BR" datatype="plaintext">
@@ -137,6 +137,11 @@
<target>%@ хочет соединиться!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<target>%1$@, %2$@</target>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<target>%@, %@ и %lld членов группы</target>
@@ -157,11 +162,36 @@
<target>%d дней</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve">
<source>%d file(s) are still being downloaded.</source>
<target>%d файл(ов) загружаются.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) failed to download." xml:space="preserve">
<source>%d file(s) failed to download.</source>
<target>%d файл(ов) не удалось загрузить.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were deleted." xml:space="preserve">
<source>%d file(s) were deleted.</source>
<target>%d файлов было удалено.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve">
<source>%d file(s) were not downloaded.</source>
<target>%d файлов не было загружено.</target>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d hours" xml:space="preserve">
<source>%d hours</source>
<target>%d ч.</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d messages not forwarded" xml:space="preserve">
<source>%d messages not forwarded</source>
<target>%d сообщений не переслано</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="%d min" xml:space="preserve">
<source>%d min</source>
<target>%d мин</target>
@@ -723,7 +753,7 @@
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<target>Все профили</target>
<note>No comment provided by engineer.</note>
<note>profile dropdown</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source>
@@ -915,6 +945,11 @@
<target>Код доступа в приложение будет заменен кодом самоуничтожения.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App session" xml:space="preserve">
<source>App session</source>
<target>Сессия приложения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App version" xml:space="preserve">
<source>App version</source>
<target>Версия приложения</target>
@@ -1020,6 +1055,11 @@
<target>Автоприем изображений</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<target>Настройки автоприема</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
<source>Back</source>
<target>Назад</target>
@@ -1045,11 +1085,21 @@
<target>Ошибка хэш сообщения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source>
<target>Улучшенные звонки</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<target>Улучшенные группы</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source>
<target>Улучшенные даты сообщений.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>Улучшенные сообщения</target>
@@ -1060,6 +1110,21 @@
<target>Улучшенные сетевые функции</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source>
<target>Улучшенные уведомления</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source>
<target>Улучшенная безопасность ✅</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source>
<target>Улучшенный интерфейс</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Black" xml:space="preserve">
<source>Black</source>
<target>Черная</target>
@@ -1193,7 +1258,7 @@
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>Отменить</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve">
<source>Cancel migration</source>
@@ -1213,7 +1278,7 @@
<trans-unit id="Cannot receive file" xml:space="preserve">
<source>Cannot receive file</source>
<target>Невозможно получить файл</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
@@ -1341,6 +1406,16 @@
<target>Предпочтения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences were changed." xml:space="preserve">
<source>Chat preferences were changed.</source>
<target>Настройки чата были изменены.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Chat profile" xml:space="preserve">
<source>Chat profile</source>
<target>Профиль чата</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat theme" xml:space="preserve">
<source>Chat theme</source>
<target>Тема чата</target>
@@ -1418,7 +1493,7 @@
</trans-unit>
<trans-unit id="Color chats with the new themes." xml:space="preserve">
<source>Color chats with the new themes.</source>
<target>Добавьте цвета к чатам в настройках тем.</target>
<target>Добавьте цвета к чатам в настройках.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Color mode" xml:space="preserve">
@@ -1740,6 +1815,11 @@ This is your own one-time link!</source>
<target>Версия ядра: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<target>Угол</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<target>Исправить имя на %@?</target>
@@ -1865,6 +1945,11 @@ This is your own one-time link!</source>
<target>Пользовательское время</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source>
<target>Настраиваемая форма сообщений.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customize theme" xml:space="preserve">
<source>Customize theme</source>
<target>Настроить тему</target>
@@ -2159,6 +2244,11 @@ This is your own one-time link!</source>
<target>Удалить предыдущую версию данных?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source>
<target>Удаляйте или модерируйте до 200 сообщений.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve">
<source>Delete pending connection?</source>
<target>Удалить ожидаемое соединение?</target>
@@ -2176,7 +2266,7 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Delete up to 20 messages at once." xml:space="preserve">
<source>Delete up to 20 messages at once.</source>
<target>Удаляйте до 20 сообщений одновременно.</target>
<target>Удаляйте до 20 сообщений за раз.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete user profile?" xml:space="preserve">
@@ -2409,6 +2499,11 @@ This is your own one-time link!</source>
<target>Не отправлять историю новым членам.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not use credentials with proxy." xml:space="preserve">
<source>Do not use credentials with proxy.</source>
<target>Не использовать учетные данные с прокси.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>Не создавать адрес</target>
@@ -2432,7 +2527,8 @@ This is your own one-time link!</source>
<trans-unit id="Download" xml:space="preserve">
<source>Download</source>
<target>Загрузить</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Download errors" xml:space="preserve">
<source>Download errors</source>
@@ -2449,6 +2545,11 @@ This is your own one-time link!</source>
<target>Загрузка файла</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Download files" xml:space="preserve">
<source>Download files</source>
<target>Загрузить файлы</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Downloaded" xml:space="preserve">
<source>Downloaded</source>
<target>Принято</target>
@@ -2724,6 +2825,11 @@ This is your own one-time link!</source>
<target>Ошибка при изменении адреса</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<target>Ошибка при изменении профиля соединения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>Ошибка при изменении роли</target>
@@ -2734,6 +2840,11 @@ This is your own one-time link!</source>
<target>Ошибка при изменении настройки</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<target>Ошибка при смене на Инкогнито!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<target>Ошибка подключения к пересылающему серверу %@. Попробуйте позже.</target>
@@ -2854,6 +2965,11 @@ This is your own one-time link!</source>
<target>Ошибка загрузки %@ серверов</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error migrating settings" xml:space="preserve">
<source>Error migrating settings</source>
<target>Ошибка миграции настроек</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<target>Ошибка доступа к чату</target>
@@ -2862,7 +2978,7 @@ This is your own one-time link!</source>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>Ошибка при получении файла</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Error reconnecting server" xml:space="preserve">
<source>Error reconnecting server</source>
@@ -2954,10 +3070,15 @@ This is your own one-time link!</source>
<target>Ошибка при остановке чата</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<target>Ошибка переключения профиля</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>Ошибка выбора профиля!</target>
<note>No comment provided by engineer.</note>
<note>alertTitle</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
@@ -3002,8 +3123,7 @@ This is your own one-time link!</source>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>Ошибка: %@</target>
<note>file error text
snd error text</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Error: URL is invalid" xml:space="preserve">
<source>Error: URL is invalid</source>
@@ -3090,6 +3210,13 @@ This is your own one-time link!</source>
<target>Ошибка файла</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File errors:&#10;%@" xml:space="preserve">
<source>File errors:
%@</source>
<target>Ошибки файлов:
%@</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve">
<source>File not found - most likely file was deleted or cancelled.</source>
<target>Файл не найден - скорее всего, файл был удален или отменен.</target>
@@ -3225,11 +3352,31 @@ This is your own one-time link!</source>
<target>Переслать</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Forward %d message(s)?" xml:space="preserve">
<source>Forward %d message(s)?</source>
<target>Переслать %d сообщение(й)?</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Forward and save messages" xml:space="preserve">
<source>Forward and save messages</source>
<target>Переслать и сохранить сообщение</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forward messages" xml:space="preserve">
<source>Forward messages</source>
<target>Переслать сообщения</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Forward messages without files?" xml:space="preserve">
<source>Forward messages without files?</source>
<target>Переслать сообщения без файлов?</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source>
<target>Пересылайте до 20 сообщений за раз.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarded" xml:space="preserve">
<source>Forwarded</source>
<target>Переслано</target>
@@ -3240,6 +3387,11 @@ This is your own one-time link!</source>
<target>Переслано из</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding %lld messages" xml:space="preserve">
<source>Forwarding %lld messages</source>
<target>Пересылка %lld сообщений</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve">
<source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source>
<target>Пересылающий сервер %@ не смог подключиться к серверу назначения %@. Попробуйте позже.</target>
@@ -3289,11 +3441,6 @@ Error: %2$@</source>
<target>Полное имя (не обязательно)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Full name:" xml:space="preserve">
<source>Full name:</source>
<target>Полное имя:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<target>Группа полностью децентрализована – она видна только членам.</target>
@@ -3539,6 +3686,11 @@ Error: %2$@</source>
<target>ICE серверы (один на строке)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="IP address" xml:space="preserve">
<source>IP address</source>
<target>IP адрес</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
<target>Если Вы не можете встретиться лично, покажите QR-код во время видеозвонка или поделитесь ссылкой.</target>
@@ -3614,6 +3766,12 @@ Error: %2$@</source>
<target>Импорт архива</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source>
<target>Улучшенная доставка, меньше трафик.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<target>Улучшенная доставка сообщений</target>
@@ -4184,6 +4342,11 @@ This is your link for group %@!</source>
<target>Серверы сообщений</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<target>Форма сообщений</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<target>Источник сообщения остаётся конфиденциальным.</target>
@@ -4234,6 +4397,11 @@ This is your link for group %@!</source>
<target>Сообщений отправлено</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages were deleted after you selected them." xml:space="preserve">
<source>Messages were deleted after you selected them.</source>
<target>Сообщения были удалены после того, как вы их выбрали.</target>
<note>alert message</note>
</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">
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
<target>Сообщения, файлы и звонки защищены **end-to-end шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома.</target>
@@ -4389,6 +4557,16 @@ This is your link for group %@!</source>
<target>Новый Код</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve">
<source>New SOCKS credentials will be used every time you start the app.</source>
<target>Новые учетные данные SOCKS будут использоваться при каждом запуске приложения.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve">
<source>New SOCKS credentials will be used for each server.</source>
<target>Новые учетные данные SOCKS будут использоваться для каждого сервера.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<target>Новый чат</target>
@@ -4509,6 +4687,16 @@ This is your link for group %@!</source>
<target>Нет интернет-соединения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record speech" xml:space="preserve">
<source>No permission to record speech</source>
<target>Нет разрешения на запись речи</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record video" xml:space="preserve">
<source>No permission to record video</source>
<target>Нет разрешения на запись видео</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>Нет разрешения для записи голосового сообщения</target>
@@ -4529,6 +4717,11 @@ This is your link for group %@!</source>
<target>Ничего не выбрано</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Nothing to forward!" xml:space="preserve">
<source>Nothing to forward!</source>
<target>Нет сообщений, которые можно переслать!</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>Уведомления</target>
@@ -4561,7 +4754,7 @@ This is your link for group %@!</source>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>Ок</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Old database" xml:space="preserve">
<source>Old database</source>
@@ -4752,6 +4945,13 @@ Requires compatible VPN.</source>
<target>Другие %@ серверы</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other file errors:&#10;%@" xml:space="preserve">
<source>Other file errors:
%@</source>
<target>Другие ошибки файлов:
%@</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>Количество PING</target>
@@ -4787,6 +4987,11 @@ Requires compatible VPN.</source>
<target>Код доступа установлен!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password" xml:space="preserve">
<source>Password</source>
<target>Пароль</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password to show" xml:space="preserve">
<source>Password to show</source>
<target>Пароль чтобы раскрыть</target>
@@ -4936,6 +5141,11 @@ Error: %@</source>
<target>Польский интерфейс</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Port" xml:space="preserve">
<source>Port</source>
<target>Порт</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve">
<source>Possibly, certificate fingerprint in server address is incorrect</source>
<target>Возможно, хэш сертификата в адресе сервера неверный</target>
@@ -4988,7 +5198,7 @@ Error: %@</source>
</trans-unit>
<trans-unit id="Private message routing 🚀" xml:space="preserve">
<source>Private message routing 🚀</source>
<target>Конфиденциальная доставка сообщений 🚀</target>
<target>Конфиденциальная доставка 🚀</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Private notes" xml:space="preserve">
@@ -5021,16 +5231,6 @@ Error: %@</source>
<target>Картинки профилей</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<target>Имя профиля</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<target>Имя профиля:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source>
<target>Пароль профиля</target>
@@ -5133,6 +5333,11 @@ Enable in *Network &amp; servers* settings.</source>
<target>Проксированные серверы</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Proxy requires password" xml:space="preserve">
<source>Proxy requires password</source>
<target>Прокси требует пароль</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>Доставка уведомлений</target>
@@ -5354,6 +5559,11 @@ Enable in *Network &amp; servers* settings.</source>
<target>Удалить</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
<target>Удалить архив?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove image" xml:space="preserve">
<source>Remove image</source>
<target>Удалить изображение</target>
@@ -5534,6 +5744,11 @@ Enable in *Network &amp; servers* settings.</source>
<target>SMP сервер</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SOCKS proxy" xml:space="preserve">
<source>SOCKS proxy</source>
<target>SOCKS прокси</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Safely receive files" xml:space="preserve">
<source>Safely receive files</source>
<target>Получайте файлы безопасно</target>
@@ -5547,12 +5762,13 @@ Enable in *Network &amp; servers* settings.</source>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<target>Сохранить</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
<source>Save (and notify contacts)</source>
<target>Сохранить (и уведомить контакты)</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Save and notify contact" xml:space="preserve">
<source>Save and notify contact</source>
@@ -5579,11 +5795,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>Сохранить архив</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save auto-accept settings" xml:space="preserve">
<source>Save auto-accept settings</source>
<target>Сохранить настройки автоприема</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save group profile" xml:space="preserve">
<source>Save group profile</source>
<target>Сохранить профиль группы</target>
@@ -5619,16 +5830,16 @@ Enable in *Network &amp; servers* settings.</source>
<target>Сохранить серверы?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save settings?" xml:space="preserve">
<source>Save settings?</source>
<target>Сохранить настройки?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save welcome message?" xml:space="preserve">
<source>Save welcome message?</source>
<target>Сохранить приветственное сообщение?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save your profile?" xml:space="preserve">
<source>Save your profile?</source>
<target>Сохранить ваш профиль?</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Saved" xml:space="preserve">
<source>Saved</source>
<target>Сохранено</target>
@@ -5649,6 +5860,11 @@ Enable in *Network &amp; servers* settings.</source>
<target>Сохраненное сообщение</target>
<note>message info title</note>
</trans-unit>
<trans-unit id="Saving %lld messages" xml:space="preserve">
<source>Saving %lld messages</source>
<target>Сохранение %lld сообщений</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scale" xml:space="preserve">
<source>Scale</source>
<target>Масштаб</target>
@@ -5729,6 +5945,11 @@ Enable in *Network &amp; servers* settings.</source>
<target>Выбрать</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<target>Выберите профиль чата</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<target>Выбрано %lld</target>
@@ -5847,7 +6068,7 @@ Enable in *Network &amp; servers* settings.</source>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>Отправитель отменил передачу файла.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Sender may have deleted the connection request." xml:space="preserve">
<source>Sender may have deleted the connection request.</source>
@@ -5944,6 +6165,11 @@ Enable in *Network &amp; servers* settings.</source>
<target>Отправлено через прокси</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<target>Сервер</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server address" xml:space="preserve">
<source>Server address</source>
<target>Адрес сервера</target>
@@ -6064,6 +6290,11 @@ Enable in *Network &amp; servers* settings.</source>
<target>Настройки</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Settings were changed." xml:space="preserve">
<source>Settings were changed.</source>
<target>Настройки были изменены.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Shape profile images" xml:space="preserve">
<source>Shape profile images</source>
<target>Форма картинок профилей</target>
@@ -6099,6 +6330,11 @@ Enable in *Network &amp; servers* settings.</source>
<target>Поделиться ссылкой</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<target>Поделиться профилем</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<target>Поделиться одноразовой ссылкой-приглашением</target>
@@ -6234,6 +6470,11 @@ Enable in *Network &amp; servers* settings.</source>
<target>SimpleX одноразовая ссылка</target>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<target>Аудит SimpleX протоколов от Trail of Bits.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Упрощенный режим Инкогнито</target>
@@ -6264,6 +6505,11 @@ Enable in *Network &amp; servers* settings.</source>
<target>Слабое</target>
<note>blur media</note>
</trans-unit>
<trans-unit id="Some app settings were not migrated." xml:space="preserve">
<source>Some app settings were not migrated.</source>
<target>Некоторые настройки приложения не были перенесены.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some file(s) were not exported:" xml:space="preserve">
<source>Some file(s) were not exported:</source>
<target>Некоторые файл(ы) не были экспортированы:</target>
@@ -6404,6 +6650,16 @@ Enable in *Network &amp; servers* settings.</source>
<target>Поддержать SimpleX Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source>
<target>Переключайте звук и видео во время звонка.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source>
<target>Переключайте профиль чата для одноразовых приглашений.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>Системная</target>
@@ -6439,6 +6695,11 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<target>Хвост</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>Сделать фото</target>
@@ -6631,6 +6892,11 @@ It can happen because of some bug or when the connection is compromised.</source
<target>Вставленный текст не является SimpleX-ссылкой.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve">
<source>The uploaded database archive will be permanently removed from the servers.</source>
<target>Загруженный архив базы данных будет навсегда удален с серверов.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Themes" xml:space="preserve">
<source>Themes</source>
<target>Темы</target>
@@ -6758,6 +7024,16 @@ You will be prompted to complete authentication before this feature is enabled.<
Вам будет нужно пройти аутентификацию для включения блокировки.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve">
<source>To record speech please grant permission to use Microphone.</source>
<target>Для записи речи, пожалуйста, дайте разрешение на использование микрофона.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record video please grant permission to use Camera." xml:space="preserve">
<source>To record video please grant permission to use Camera.</source>
<target>Для записи видео, пожалуйста, дайте разрешение на использование камеры.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve">
<source>To record voice message please grant permission to use Microphone.</source>
<target>Для записи голосового сообщения, пожалуйста разрешите доступ к микрофону.</target>
@@ -6911,7 +7187,7 @@ You will be prompted to complete authentication before this feature is enabled.<
<trans-unit id="Unknown servers!" xml:space="preserve">
<source>Unknown servers!</source>
<target>Неизвестные серверы!</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
@@ -7025,6 +7301,11 @@ To connect, please ask your contact to create another connection link and check
<target>Использовать .onion хосты</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SOCKS proxy" xml:space="preserve">
<source>Use SOCKS proxy</source>
<target>Использовать SOCKS прокси</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
<source>Use SimpleX Chat servers?</source>
<target>Использовать серверы предосталенные SimpleX Chat?</target>
@@ -7090,16 +7371,16 @@ To connect, please ask your contact to create another connection link and check
<target>Используйте приложение одной рукой.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User profile" xml:space="preserve">
<source>User profile</source>
<target>Профиль чата</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User selection" xml:space="preserve">
<source>User selection</source>
<target>Выбор пользователя</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<target>Имя пользователя</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
<source>Using SimpleX Chat servers.</source>
<target>Используются серверы, предоставленные SimpleX Chat.</target>
@@ -7333,7 +7614,7 @@ To connect, please ask your contact to create another connection link and check
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
<target>Без Тора или ВПН, Ваш IP адрес будет доступен этим серверам файлов: %@.</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
@@ -7360,11 +7641,6 @@ To connect, please ask your contact to create another connection link and check
<target>XFTP сервер</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You" xml:space="preserve">
<source>You</source>
<target>Вы</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
<source>You **must not** use the same database on two devices.</source>
<target>Вы **не должны** использовать одну и ту же базу данных на двух устройствах.</target>
@@ -7714,11 +7990,21 @@ Repeat connection request?</source>
<target>База данных НЕ зашифрована. Установите пароль, чтобы защитить Ваши данные.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat preferences" xml:space="preserve">
<source>Your chat preferences</source>
<target>Ваши настройки чата</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>Ваши профили чата</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<target>Соединение было перемещено на %@, но при смене профиля произошла неожиданная ошибка.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>Ваш контакт отправил файл, размер которого превышает максимальный размер (%@).</target>
@@ -7734,6 +8020,11 @@ Repeat connection request?</source>
<target>Ваши контакты сохранятся.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve">
<source>Your credentials may be sent unencrypted.</source>
<target>Ваши учетные данные могут быть отправлены в незашифрованном виде.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve">
<source>Your current chat database will be DELETED and REPLACED with the imported one.</source>
<target>Текущие данные Вашего чата будет УДАЛЕНЫ и ЗАМЕНЕНЫ импортированными.</target>
@@ -7764,13 +8055,16 @@ Repeat connection request?</source>
<target>Будет отправлен Ваш профиль **%@**.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
<target>Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам.
SimpleX серверы не могут получить доступ к Вашему профилю.</target>
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
<target>Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. SimpleX серверы не могут получить доступ к Вашему профилю.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve">
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
<target>Ваш профиль был изменен. Если вы сохраните его, обновленный профиль будет отправлен всем вашим контактам.</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве.</target>
@@ -129,6 +129,10 @@
<target>%@ อยากเชื่อมต่อ!</target>
<note>notification title</note>
</trans-unit>
<trans-unit id="%@, %@" xml:space="preserve">
<source>%1$@, %2$@</source>
<note>format for date separator in chat</note>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve">
<source>%@, %@ and %lld members</source>
<note>No comment provided by engineer.</note>
@@ -147,11 +151,31 @@
<target>%d วัน</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d file(s) are still being downloaded." xml:space="preserve">
<source>%d file(s) are still being downloaded.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) failed to download." xml:space="preserve">
<source>%d file(s) failed to download.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were deleted." xml:space="preserve">
<source>%d file(s) were deleted.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d file(s) were not downloaded." xml:space="preserve">
<source>%d file(s) were not downloaded.</source>
<note>forward confirmation reason</note>
</trans-unit>
<trans-unit id="%d hours" xml:space="preserve">
<source>%d hours</source>
<target>%d ชั่วโมง</target>
<note>time interval</note>
</trans-unit>
<trans-unit id="%d messages not forwarded" xml:space="preserve">
<source>%d messages not forwarded</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="%d min" xml:space="preserve">
<source>%d min</source>
<target>%d นาที</target>
@@ -678,7 +702,7 @@
</trans-unit>
<trans-unit id="All profiles" xml:space="preserve">
<source>All profiles</source>
<note>No comment provided by engineer.</note>
<note>profile dropdown</note>
</trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve">
<source>All your contacts will remain connected.</source>
@@ -860,6 +884,10 @@
<target>รหัสผ่านแอปจะถูกแทนที่ด้วยรหัสผ่านที่ทำลายตัวเอง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App session" xml:space="preserve">
<source>App session</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="App version" xml:space="preserve">
<source>App version</source>
<target>เวอร์ชันแอป</target>
@@ -959,6 +987,10 @@
<target>ยอมรับภาพอัตโนมัติ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
<source>Back</source>
<target>กลับ</target>
@@ -982,10 +1014,18 @@
<target>แฮชข้อความไม่ดี</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better calls" xml:space="preserve">
<source>Better calls</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve">
<source>Better groups</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better message dates." xml:space="preserve">
<source>Better message dates.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better messages" xml:space="preserve">
<source>Better messages</source>
<target>ข้อความที่ดีขึ้น</target>
@@ -995,6 +1035,18 @@
<source>Better networking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better notifications" xml:space="preserve">
<source>Better notifications</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better security ✅" xml:space="preserve">
<source>Better security ✅</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Better user experience" xml:space="preserve">
<source>Better user experience</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Black" xml:space="preserve">
<source>Black</source>
<note>No comment provided by engineer.</note>
@@ -1112,7 +1164,7 @@
<trans-unit id="Cancel" xml:space="preserve">
<source>Cancel</source>
<target>ยกเลิก</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve">
<source>Cancel migration</source>
@@ -1130,7 +1182,7 @@
<trans-unit id="Cannot receive file" xml:space="preserve">
<source>Cannot receive file</source>
<target>ไม่สามารถรับไฟล์ได้</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Capacity exceeded - recipient did not receive previously sent messages." xml:space="preserve">
<source>Capacity exceeded - recipient did not receive previously sent messages.</source>
@@ -1251,6 +1303,15 @@
<target>ค่ากําหนดในการแชท</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat preferences were changed." xml:space="preserve">
<source>Chat preferences were changed.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Chat profile" xml:space="preserve">
<source>Chat profile</source>
<target>โปรไฟล์ผู้ใช้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Chat theme" xml:space="preserve">
<source>Chat theme</source>
<note>No comment provided by engineer.</note>
@@ -1606,6 +1667,10 @@ This is your own one-time link!</source>
<target>รุ่นหลัก: v%@</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Corner" xml:space="preserve">
<source>Corner</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Correct name to %@?" xml:space="preserve">
<source>Correct name to %@?</source>
<note>No comment provided by engineer.</note>
@@ -1720,6 +1785,10 @@ This is your own one-time link!</source>
<target>เวลาที่กําหนดเอง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customizable message shape." xml:space="preserve">
<source>Customizable message shape.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Customize theme" xml:space="preserve">
<source>Customize theme</source>
<note>No comment provided by engineer.</note>
@@ -2006,6 +2075,10 @@ This is your own one-time link!</source>
<target>ลบฐานข้อมูลเก่า?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete or moderate up to 200 messages." xml:space="preserve">
<source>Delete or moderate up to 200 messages.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete pending connection?" xml:space="preserve">
<source>Delete pending connection?</source>
<target>ลบการเชื่อมต่อที่รอดำเนินการหรือไม่?</target>
@@ -2235,6 +2308,10 @@ This is your own one-time link!</source>
<source>Do not send history to new members.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Do not use credentials with proxy." xml:space="preserve">
<source>Do not use credentials with proxy.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Don't create address" xml:space="preserve">
<source>Don't create address</source>
<target>อย่าสร้างที่อยู่</target>
@@ -2257,7 +2334,8 @@ This is your own one-time link!</source>
</trans-unit>
<trans-unit id="Download" xml:space="preserve">
<source>Download</source>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Download errors" xml:space="preserve">
<source>Download errors</source>
@@ -2272,6 +2350,10 @@ This is your own one-time link!</source>
<target>ดาวน์โหลดไฟล์</target>
<note>server test step</note>
</trans-unit>
<trans-unit id="Download files" xml:space="preserve">
<source>Download files</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Downloaded" xml:space="preserve">
<source>Downloaded</source>
<note>No comment provided by engineer.</note>
@@ -2530,6 +2612,10 @@ This is your own one-time link!</source>
<target>เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing connection profile" xml:space="preserve">
<source>Error changing connection profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing role" xml:space="preserve">
<source>Error changing role</source>
<target>เกิดข้อผิดพลาดในการเปลี่ยนบทบาท</target>
@@ -2540,6 +2626,10 @@ This is your own one-time link!</source>
<target>เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error changing to incognito!" xml:space="preserve">
<source>Error changing to incognito!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error connecting to forwarding server %@. Please try later." xml:space="preserve">
<source>Error connecting to forwarding server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -2654,6 +2744,10 @@ This is your own one-time link!</source>
<target>โหลดเซิร์ฟเวอร์ %@ ผิดพลาด</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error migrating settings" xml:space="preserve">
<source>Error migrating settings</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error opening chat" xml:space="preserve">
<source>Error opening chat</source>
<note>No comment provided by engineer.</note>
@@ -2661,7 +2755,7 @@ This is your own one-time link!</source>
<trans-unit id="Error receiving file" xml:space="preserve">
<source>Error receiving file</source>
<target>เกิดข้อผิดพลาดในการรับไฟล์</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Error reconnecting server" xml:space="preserve">
<source>Error reconnecting server</source>
@@ -2747,10 +2841,14 @@ This is your own one-time link!</source>
<target>เกิดข้อผิดพลาดในการหยุดแชท</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile" xml:space="preserve">
<source>Error switching profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error switching profile!" xml:space="preserve">
<source>Error switching profile!</source>
<target>เกิดข้อผิดพลาดในการเปลี่ยนโปรไฟล์!</target>
<note>No comment provided by engineer.</note>
<note>alertTitle</note>
</trans-unit>
<trans-unit id="Error synchronizing connection" xml:space="preserve">
<source>Error synchronizing connection</source>
@@ -2793,8 +2891,7 @@ This is your own one-time link!</source>
<trans-unit id="Error: %@" xml:space="preserve">
<source>Error: %@</source>
<target>ข้อผิดพลาด: % @</target>
<note>file error text
snd error text</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Error: URL is invalid" xml:space="preserve">
<source>Error: URL is invalid</source>
@@ -2875,6 +2972,11 @@ This is your own one-time link!</source>
<source>File error</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="File errors:&#10;%@" xml:space="preserve">
<source>File errors:
%@</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="File not found - most likely file was deleted or cancelled." xml:space="preserve">
<source>File not found - most likely file was deleted or cancelled.</source>
<note>file error text</note>
@@ -3001,10 +3103,26 @@ This is your own one-time link!</source>
<source>Forward</source>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Forward %d message(s)?" xml:space="preserve">
<source>Forward %d message(s)?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Forward and save messages" xml:space="preserve">
<source>Forward and save messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forward messages" xml:space="preserve">
<source>Forward messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Forward messages without files?" xml:space="preserve">
<source>Forward messages without files?</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Forward up to 20 messages at once." xml:space="preserve">
<source>Forward up to 20 messages at once.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarded" xml:space="preserve">
<source>Forwarded</source>
<note>No comment provided by engineer.</note>
@@ -3013,6 +3131,10 @@ This is your own one-time link!</source>
<source>Forwarded from</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding %lld messages" xml:space="preserve">
<source>Forwarding %lld messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Forwarding server %@ failed to connect to destination server %@. Please try later." xml:space="preserve">
<source>Forwarding server %@ failed to connect to destination server %@. Please try later.</source>
<note>No comment provided by engineer.</note>
@@ -3054,11 +3176,6 @@ Error: %2$@</source>
<target>ชื่อเต็ม (ไม่บังคับ)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Full name:" xml:space="preserve">
<source>Full name:</source>
<target>ชื่อเต็ม:</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Fully decentralized visible only to members." xml:space="preserve">
<source>Fully decentralized visible only to members.</source>
<note>No comment provided by engineer.</note>
@@ -3296,6 +3413,10 @@ Error: %2$@</source>
<target>เซิร์ฟเวอร์ ICE (หนึ่งเครื่องต่อสาย)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="IP address" xml:space="preserve">
<source>IP address</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
<target>หากคุณไม่สามารถพบกันในชีวิตจริงได้ ให้แสดงคิวอาร์โค้ดในวิดีโอคอล หรือแชร์ลิงก์</target>
@@ -3368,6 +3489,11 @@ Error: %2$@</source>
<source>Importing archive</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved delivery, reduced traffic usage.&#10;More improvements are coming soon!" xml:space="preserve">
<source>Improved delivery, reduced traffic usage.
More improvements are coming soon!</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source>
<note>No comment provided by engineer.</note>
@@ -3901,6 +4027,10 @@ This is your link for group %@!</source>
<source>Message servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message shape" xml:space="preserve">
<source>Message shape</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Message source remains private." xml:space="preserve">
<source>Message source remains private.</source>
<note>No comment provided by engineer.</note>
@@ -3944,6 +4074,10 @@ This is your link for group %@!</source>
<source>Messages sent</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Messages were deleted after you selected them." xml:space="preserve">
<source>Messages were deleted after you selected them.</source>
<note>alert message</note>
</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">
<source>Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.</source>
<note>No comment provided by engineer.</note>
@@ -4085,6 +4219,14 @@ This is your link for group %@!</source>
<target>รหัสผ่านใหม่</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used every time you start the app." xml:space="preserve">
<source>New SOCKS credentials will be used every time you start the app.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New SOCKS credentials will be used for each server." xml:space="preserve">
<source>New SOCKS credentials will be used for each server.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="New chat" xml:space="preserve">
<source>New chat</source>
<note>No comment provided by engineer.</note>
@@ -4197,6 +4339,14 @@ This is your link for group %@!</source>
<source>No network connection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record speech" xml:space="preserve">
<source>No permission to record speech</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record video" xml:space="preserve">
<source>No permission to record video</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="No permission to record voice message" xml:space="preserve">
<source>No permission to record voice message</source>
<target>ไม่อนุญาตให้บันทึกข้อความเสียง</target>
@@ -4215,6 +4365,10 @@ This is your link for group %@!</source>
<source>Nothing selected</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Nothing to forward!" xml:space="preserve">
<source>Nothing to forward!</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source>
<target>การแจ้งเตือน</target>
@@ -4246,7 +4400,7 @@ This is your link for group %@!</source>
<trans-unit id="Ok" xml:space="preserve">
<source>Ok</source>
<target>ตกลง</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Old database" xml:space="preserve">
<source>Old database</source>
@@ -4423,6 +4577,11 @@ Requires compatible VPN.</source>
<source>Other %@ servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Other file errors:&#10;%@" xml:space="preserve">
<source>Other file errors:
%@</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="PING count" xml:space="preserve">
<source>PING count</source>
<target>จํานวน PING</target>
@@ -4458,6 +4617,10 @@ Requires compatible VPN.</source>
<target>ตั้งรหัสผ่านเรียบร้อยแล้ว!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password" xml:space="preserve">
<source>Password</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Password to show" xml:space="preserve">
<source>Password to show</source>
<target>รหัสผ่านที่จะแสดง</target>
@@ -4594,6 +4757,10 @@ Error: %@</source>
<target>อินเตอร์เฟซภาษาโปแลนด์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Port" xml:space="preserve">
<source>Port</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve">
<source>Possibly, certificate fingerprint in server address is incorrect</source>
<target>อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง</target>
@@ -4672,14 +4839,6 @@ Error: %@</source>
<source>Profile images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name" xml:space="preserve">
<source>Profile name</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile name:" xml:space="preserve">
<source>Profile name:</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Profile password" xml:space="preserve">
<source>Profile password</source>
<target>รหัสผ่านโปรไฟล์</target>
@@ -4775,6 +4934,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Proxied servers</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Proxy requires password" xml:space="preserve">
<source>Proxy requires password</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Push notifications" xml:space="preserve">
<source>Push notifications</source>
<target>การแจ้งเตือนแบบทันที</target>
@@ -4979,6 +5142,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>ลบ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove image" xml:space="preserve">
<source>Remove image</source>
<note>No comment provided by engineer.</note>
@@ -5146,6 +5313,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>SMP server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SOCKS proxy" xml:space="preserve">
<source>SOCKS proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Safely receive files" xml:space="preserve">
<source>Safely receive files</source>
<note>No comment provided by engineer.</note>
@@ -5157,12 +5328,13 @@ Enable in *Network &amp; servers* settings.</source>
<trans-unit id="Save" xml:space="preserve">
<source>Save</source>
<target>บันทึก</target>
<note>chat item action</note>
<note>alert button
chat item action</note>
</trans-unit>
<trans-unit id="Save (and notify contacts)" xml:space="preserve">
<source>Save (and notify contacts)</source>
<target>บันทึก (และแจ้งผู้ติดต่อ)</target>
<note>No comment provided by engineer.</note>
<note>alert button</note>
</trans-unit>
<trans-unit id="Save and notify contact" xml:space="preserve">
<source>Save and notify contact</source>
@@ -5188,11 +5360,6 @@ Enable in *Network &amp; servers* settings.</source>
<target>บันทึกไฟล์เก็บถาวร</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save auto-accept settings" xml:space="preserve">
<source>Save auto-accept settings</source>
<target>บันทึกการตั้งค่าการยอมรับอัตโนมัติ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save group profile" xml:space="preserve">
<source>Save group profile</source>
<target>บันทึกโปรไฟล์กลุ่ม</target>
@@ -5228,16 +5395,15 @@ Enable in *Network &amp; servers* settings.</source>
<target>บันทึกเซิร์ฟเวอร์?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save settings?" xml:space="preserve">
<source>Save settings?</source>
<target>บันทึกการตั้งค่า?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save welcome message?" xml:space="preserve">
<source>Save welcome message?</source>
<target>บันทึกข้อความต้อนรับ?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Save your profile?" xml:space="preserve">
<source>Save your profile?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Saved" xml:space="preserve">
<source>Saved</source>
<note>No comment provided by engineer.</note>
@@ -5255,6 +5421,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Saved message</source>
<note>message info title</note>
</trans-unit>
<trans-unit id="Saving %lld messages" xml:space="preserve">
<source>Saving %lld messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Scale" xml:space="preserve">
<source>Scale</source>
<note>No comment provided by engineer.</note>
@@ -5328,6 +5498,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>เลือก</target>
<note>chat item action</note>
</trans-unit>
<trans-unit id="Select chat profile" xml:space="preserve">
<source>Select chat profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Selected %lld" xml:space="preserve">
<source>Selected %lld</source>
<note>No comment provided by engineer.</note>
@@ -5438,7 +5612,7 @@ Enable in *Network &amp; servers* settings.</source>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
<source>Sender cancelled file transfer.</source>
<target>ผู้ส่งยกเลิกการโอนไฟล์</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Sender may have deleted the connection request." xml:space="preserve">
<source>Sender may have deleted the connection request.</source>
@@ -5528,6 +5702,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Sent via proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server" xml:space="preserve">
<source>Server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Server address" xml:space="preserve">
<source>Server address</source>
<note>No comment provided by engineer.</note>
@@ -5637,6 +5815,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>การตั้งค่า</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Settings were changed." xml:space="preserve">
<source>Settings were changed.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Shape profile images" xml:space="preserve">
<source>Shape profile images</source>
<note>No comment provided by engineer.</note>
@@ -5670,6 +5852,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>แชร์ลิงก์</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share profile" xml:space="preserve">
<source>Share profile</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Share this 1-time invite link" xml:space="preserve">
<source>Share this 1-time invite link</source>
<note>No comment provided by engineer.</note>
@@ -5795,6 +5981,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>คำเชิญ SimpleX แบบครั้งเดียว</target>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="SimpleX protocols reviewed by Trail of Bits." xml:space="preserve">
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<note>No comment provided by engineer.</note>
@@ -5821,6 +6011,10 @@ Enable in *Network &amp; servers* settings.</source>
<source>Soft</source>
<note>blur media</note>
</trans-unit>
<trans-unit id="Some app settings were not migrated." xml:space="preserve">
<source>Some app settings were not migrated.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Some file(s) were not exported:" xml:space="preserve">
<source>Some file(s) were not exported:</source>
<note>No comment provided by engineer.</note>
@@ -5949,6 +6143,14 @@ Enable in *Network &amp; servers* settings.</source>
<target>สนับสนุน SimpleX แชท</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch audio and video during the call." xml:space="preserve">
<source>Switch audio and video during the call.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Switch chat profile for 1-time invitations." xml:space="preserve">
<source>Switch chat profile for 1-time invitations.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="System" xml:space="preserve">
<source>System</source>
<target>ระบบ</target>
@@ -5983,6 +6185,10 @@ Enable in *Network &amp; servers* settings.</source>
<target>TCP_KEEPINTVL</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Tail" xml:space="preserve">
<source>Tail</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Take picture" xml:space="preserve">
<source>Take picture</source>
<target>ถ่ายภาพ</target>
@@ -6167,6 +6373,10 @@ It can happen because of some bug or when the connection is compromised.</source
<source>The text you pasted is not a SimpleX link.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="The uploaded database archive will be permanently removed from the servers." xml:space="preserve">
<source>The uploaded database archive will be permanently removed from the servers.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Themes" xml:space="preserve">
<source>Themes</source>
<note>No comment provided by engineer.</note>
@@ -6281,6 +6491,14 @@ You will be prompted to complete authentication before this feature is enabled.<
คุณจะได้รับแจ้งให้ยืนยันตัวตนให้เสร็จสมบูรณ์ก่อนที่จะเปิดใช้งานคุณลักษณะนี้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record speech please grant permission to use Microphone." xml:space="preserve">
<source>To record speech please grant permission to use Microphone.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record video please grant permission to use Camera." xml:space="preserve">
<source>To record video please grant permission to use Camera.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve">
<source>To record voice message please grant permission to use Microphone.</source>
<target>ในการบันทึกข้อความเสียง โปรดให้สิทธิ์ในการใช้ไมโครโฟน</target>
@@ -6422,7 +6640,7 @@ You will be prompted to complete authentication before this feature is enabled.<
</trans-unit>
<trans-unit id="Unknown servers!" xml:space="preserve">
<source>Unknown servers!</source>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve">
<source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source>
@@ -6527,6 +6745,10 @@ To connect, please ask your contact to create another connection link and check
<target>ใช้โฮสต์ .onion</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SOCKS proxy" xml:space="preserve">
<source>Use SOCKS proxy</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Use SimpleX Chat servers?" xml:space="preserve">
<source>Use SimpleX Chat servers?</source>
<target>ใช้เซิร์ฟเวอร์ SimpleX Chat ไหม?</target>
@@ -6584,15 +6806,14 @@ To connect, please ask your contact to create another connection link and check
<source>Use the app with one hand.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User profile" xml:space="preserve">
<source>User profile</source>
<target>โปรไฟล์ผู้ใช้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="User selection" xml:space="preserve">
<source>User selection</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Using SimpleX Chat servers." xml:space="preserve">
<source>Using SimpleX Chat servers.</source>
<target>กำลังใช้เซิร์ฟเวอร์ SimpleX Chat อยู่</target>
@@ -6805,7 +7026,7 @@ To connect, please ask your contact to create another connection link and check
</trans-unit>
<trans-unit id="Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." xml:space="preserve">
<source>Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.</source>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve">
<source>Wrong database passphrase</source>
@@ -6829,11 +7050,6 @@ To connect, please ask your contact to create another connection link and check
<source>XFTP server</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You" xml:space="preserve">
<source>You</source>
<target>คุณ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="You **must not** use the same database on two devices." xml:space="preserve">
<source>You **must not** use the same database on two devices.</source>
<note>No comment provided by engineer.</note>
@@ -7158,11 +7374,19 @@ Repeat connection request?</source>
<target>ฐานข้อมูลการแชทของคุณไม่ได้ถูก encrypt - ตั้งรหัสผ่านเพื่อ encrypt</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your chat preferences" xml:space="preserve">
<source>Your chat preferences</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Your chat profiles" xml:space="preserve">
<source>Your chat profiles</source>
<target>โปรไฟล์แชทของคุณ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile." xml:space="preserve">
<source>Your connection was moved to %@ but an unexpected error occurred while redirecting you to the profile.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve">
<source>Your contact sent a file that is larger than currently supported maximum size (%@).</source>
<target>ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@)</target>
@@ -7178,6 +7402,10 @@ Repeat connection request?</source>
<target>ผู้ติดต่อของคุณจะยังคงเชื่อมต่ออยู่</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your credentials may be sent unencrypted." xml:space="preserve">
<source>Your credentials may be sent unencrypted.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve">
<source>Your current chat database will be DELETED and REPLACED with the imported one.</source>
<target>ฐานข้อมูลแชทปัจจุบันของคุณจะถูกลบและแทนที่ด้วยฐานข้อมูลที่นำเข้า</target>
@@ -7206,13 +7434,15 @@ Repeat connection request?</source>
<source>Your profile **%@** will be shared.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile is stored on your device and shared only with your contacts.&#10;SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts.
SimpleX servers cannot see your profile.</source>
<target>โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น
เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้</target>
<trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve">
<source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source>
<target>โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Your profile was changed. If you save it, the updated profile will be sent to all your contacts." xml:space="preserve">
<source>Your profile was changed. If you save it, the updated profile will be sent to all your contacts.</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve">
<source>Your profile, contacts and delivered messages are stored on your device.</source>
<target>โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ</target>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -314,7 +314,7 @@
</trans-unit>
<trans-unit id="About SimpleX Chat" xml:space="preserve" approved="no">
<source>About SimpleX Chat</source>
<target state="translated">關於 SimpleX 對話</target>
<target state="translated">關於 SimpleX Chat</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Accent color" xml:space="preserve" approved="no">
@@ -445,7 +445,7 @@
</trans-unit>
<trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve" approved="no">
<source>Allow your contacts to send disappearing messages.</source>
<target state="translated">允許的聯絡人傳送自動銷毀的訊息。</target>
<target state="translated">允許的聯絡人傳送限時訊息。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Allow your contacts to send voice messages." xml:space="preserve" approved="no">
@@ -5898,6 +5898,230 @@ It can happen because of some bug or when the connection is compromised.</source
<target state="translated">%@ 和 %@ 已連接</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="%@ downloaded" xml:space="preserve" approved="no">
<source>%@ downloaded</source>
<target state="translated">%@ 下載</target>
</trans-unit>
<trans-unit id="%@ uploaded" xml:space="preserve" approved="no">
<source>%@ uploaded</source>
<target state="translated">%@ 上傳</target>
</trans-unit>
<trans-unit id="Abort" xml:space="preserve" approved="no">
<source>Abort</source>
<target state="translated">中止</target>
</trans-unit>
<trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no">
<source>**Create group**: to create a new group.</source>
<target state="translated">**創建群組**: 創建一個新的群組。</target>
</trans-unit>
<trans-unit id="Abort changing address" xml:space="preserve" approved="no">
<source>Abort changing address</source>
<target state="translated">中止更改地址</target>
</trans-unit>
<trans-unit id="Accept connection request?" xml:space="preserve" approved="no">
<source>Accept connection request?</source>
<target state="translated">接受連線請求?</target>
</trans-unit>
<trans-unit id="Camera not available" xml:space="preserve" approved="no">
<source>Camera not available</source>
<target state="translated">相機不可用</target>
</trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no">
<source>All messages will be deleted - this cannot be undone!</source>
<target state="translated">所有訊息都將被刪除 - 這不能還原!</target>
</trans-unit>
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you. (24 hours)" xml:space="preserve" approved="no">
<source>Allow irreversible message deletion only if your contact allows it to you. (24 hours)</source>
<target state="translated">只有你的聯絡人允許的情況下,才允許不可逆地將訊息刪除。(24小時)</target>
</trans-unit>
<trans-unit id="Allow to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
<source>Allow to irreversibly delete sent messages. (24 hours)</source>
<target state="translated">允許將不可撤銷的訊息刪除。(24小時)</target>
</trans-unit>
<trans-unit id="Allow your contacts to irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
<source>Allow your contacts to irreversibly delete sent messages. (24 hours)</source>
<target state="translated">允許您的聯絡人不可復原地刪除已傳送的訊息。(24小時)</target>
</trans-unit>
<trans-unit id="Bad desktop address" xml:space="preserve" approved="no">
<source>Bad desktop address</source>
<target state="translated">無效的桌面地址</target>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve" approved="no">
<source>Error decrypting file</source>
<target state="translated">解密檔案時出錯</target>
</trans-unit>
<trans-unit id="Add contact" xml:space="preserve" approved="no">
<source>Add contact</source>
<target state="translated">新增聯絡人</target>
</trans-unit>
<trans-unit id="Advanced settings" xml:space="preserve" approved="no">
<source>Advanced settings</source>
<target state="translated">進階設定</target>
</trans-unit>
<trans-unit id="Allow calls?" xml:space="preserve" approved="no">
<source>Allow calls?</source>
<target state="translated">允許通話?</target>
</trans-unit>
<trans-unit id="Allow to send files and media." xml:space="preserve" approved="no">
<source>Allow to send files and media.</source>
<target state="translated">允許傳送檔案和媒體。</target>
</trans-unit>
<trans-unit id="Already joining the group!" xml:space="preserve" approved="no">
<source>Already joining the group!</source>
<target state="translated">已加入群組!</target>
</trans-unit>
<trans-unit id="App data migration" xml:space="preserve" approved="no">
<source>App data migration</source>
<target state="translated">應用資料轉移</target>
</trans-unit>
<trans-unit id="Apply" xml:space="preserve" approved="no">
<source>Apply</source>
<target state="translated">應用</target>
</trans-unit>
<trans-unit id="Apply to" xml:space="preserve" approved="no">
<source>Apply to</source>
<target state="translated">應用到</target>
</trans-unit>
<trans-unit id="Archive and upload" xml:space="preserve" approved="no">
<source>Archive and upload</source>
<target state="translated">儲存並上傳</target>
</trans-unit>
<trans-unit id="Block" xml:space="preserve" approved="no">
<source>Block</source>
<target state="translated">封鎖</target>
</trans-unit>
<trans-unit id="Block group members" xml:space="preserve" approved="no">
<source>Block group members</source>
<target state="translated">封鎖群組成員</target>
</trans-unit>
<trans-unit id="Block member" xml:space="preserve" approved="no">
<source>Block member</source>
<target state="translated">封鎖成員</target>
</trans-unit>
<trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve" approved="no">
<source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source>
<target state="translated">保加利亞語、芬蘭語、泰語和烏克蘭語——感謝使用者們和[Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)</target>
</trans-unit>
<trans-unit id="Can't call member" xml:space="preserve" approved="no">
<source>Can't call member</source>
<target state="translated">無法與成員通話</target>
</trans-unit>
<trans-unit id="Can't message member" xml:space="preserve" approved="no">
<source>Can't message member</source>
<target state="translated">無法傳送訊息給成員</target>
</trans-unit>
<trans-unit id="Cancel migration" xml:space="preserve" approved="no">
<source>Cancel migration</source>
<target state="translated">取消遷移</target>
</trans-unit>
<trans-unit id="Chat database exported" xml:space="preserve" approved="no">
<source>Chat database exported</source>
<target state="translated">導出聊天數據庫</target>
</trans-unit>
<trans-unit id="0 sec" xml:space="preserve" approved="no">
<source>0 sec</source>
<target state="translated">0 秒</target>
</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" approved="no">
<source>All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</source>
<target state="translated">你的所有聯絡人、對話和文件將被安全加密並切塊上傳到設置的 XFTP 中繼。</target>
</trans-unit>
<trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve" approved="no">
<source>Address change will be aborted. Old receiving address will be used.</source>
<target state="translated">將取消地址更改。將使用舊聯絡地址。</target>
</trans-unit>
<trans-unit id="Archiving database" xml:space="preserve" approved="no">
<source>Archiving database</source>
<target state="translated">正在儲存資料庫</target>
</trans-unit>
<trans-unit id="Cellular" xml:space="preserve" approved="no">
<source>Cellular</source>
<target state="translated">行動網路</target>
</trans-unit>
<trans-unit id="%@, %@ and %lld members" xml:space="preserve" approved="no">
<source>%@, %@ and %lld members</source>
<target state="translated">%@, %@ 和 %lld 成員</target>
</trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve" approved="no">
<source>%lld messages marked deleted</source>
<target state="translated">%lld 條訊息已刪除</target>
</trans-unit>
<trans-unit id="Already connecting!" xml:space="preserve" approved="no">
<source>Already connecting!</source>
<target state="translated">已連接!</target>
</trans-unit>
<trans-unit id="Block member?" xml:space="preserve" approved="no">
<source>Block member?</source>
<target state="translated">封鎖成員?</target>
</trans-unit>
<trans-unit id="(new)" xml:space="preserve" approved="no">
<source>(new)</source>
<target state="translated">(新)</target>
</trans-unit>
<trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no">
<source>%@, %@ and %lld other members connected</source>
<target state="translated">%@, %@ 和 %lld 個成員已連接</target>
</trans-unit>
<trans-unit id="A few more things" xml:space="preserve" approved="no">
<source>A few more things</source>
<target state="translated">其他</target>
</trans-unit>
<trans-unit id="Show last messages" xml:space="preserve" approved="no">
<source>Show last messages</source>
<target state="translated">顯示最新的訊息</target>
</trans-unit>
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve" approved="no">
<source>App encrypts new local files (except videos).</source>
<target state="translated">應用程式將為新的本機文件(影片除外)加密。</target>
</trans-unit>
<trans-unit id="Better groups" xml:space="preserve" approved="no">
<source>Better groups</source>
<target state="translated">更加的群組</target>
</trans-unit>
<trans-unit id="%lld new interface languages" xml:space="preserve" approved="no">
<source>%lld new interface languages</source>
<target state="translated">%lld 種新的介面語言</target>
</trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve" approved="no">
<source>Blocked by admin</source>
<target state="translated">由管理員封鎖</target>
</trans-unit>
<trans-unit id="Both you and your contact can irreversibly delete sent messages. (24 hours)" xml:space="preserve" approved="no">
<source>Both you and your contact can irreversibly delete sent messages. (24 hours)</source>
<target state="translated">您與您的聯絡人都可以不可復原地删除已傳送的訊息。(24小時)</target>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve" approved="no">
<source>Encrypt local files</source>
<target state="translated">加密本機檔案</target>
</trans-unit>
<trans-unit id="- more stable message delivery.&#10;- a bit better groups.&#10;- and more!" xml:space="preserve" approved="no">
<source>- more stable message delivery.
- a bit better groups.
- and more!</source>
<target state="translated">- 更穩定的傳送!
- 更好的社群!
- 以及更多!</target>
</trans-unit>
<trans-unit id="- optionally notify deleted contacts.&#10;- profile names with spaces.&#10;- and more!" xml:space="preserve" approved="no">
<source>- optionally notify deleted contacts.
- profile names with spaces.
- and more!</source>
<target state="translated">- 可選擇通知已刪除的聯絡人
- 帶空格的共人資料名稱。
-以及更多!</target>
</trans-unit>
<trans-unit id="Abort changing address?" xml:space="preserve" approved="no">
<source>Abort changing address?</source>
<target state="translated">中止更改地址?</target>
</trans-unit>
<trans-unit id="Allow to send SimpleX links." xml:space="preserve" approved="no">
<source>Allow to send SimpleX links.</source>
<target state="translated">允許傳送 SimpleX 連結。</target>
</trans-unit>
<trans-unit id="Background" xml:space="preserve" approved="no">
<source>Background</source>
<target state="translated">後台</target>
</trans-unit>
</body>
</file>
<file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="zh-Hant" datatype="plaintext">

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