mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 10:43:05 +00:00
+53









Evgeny
Claude Opus 4.8
N4TH4NOT
cedev-1
slrslr
huzaifah
kudebug
ryokky3
mlanp
Auri
Narasimha-sc
Evgeny @ SimpleX Chat
Ophiushi
Deleted User
No name
John m
Deleted User
zenobit
Maksym Lukashenko
petri
okmepro
B.O.S.S
Paulo Alexandre Pereira
Roee Hershberg
Anonymous
Titapa Chaiyakiturajai
elgratea
xe1st
Random
Hosted Weblate user 54392
summoner001
jonnysemon
Thu Pham-Anh
SquiralDot...🖤
andiasriefail
Frank Yang
Artemchik Dornikov
cllih
Aliet Expósito García
koreirozzu
Bharadwaj
Filip Ionut
Nguyễn Xuân Cảm
Dimas Alfa Pratama
Mathias Pedersen
rider of the storm
Nguyen Thanh Long
Fill Vj
Stonepilot
Bugatsinho
Darío Hereñú
Loping151
pahiy
M Yusup Hamdani
lazizkhalilov
Игорь Линчевский
J. Lavoie
Irnovi Albaweny
Hosted Weblate
SHE LINGZHAO
Rafi
kkpanfilov
echoloji
ee51168374
* ios: debug name resolution
* remove log
* ui: translations (#7210)
* Translated using Weblate (French)
Currently translated at 84.1% (2390 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/
* Translated using Weblate (Italian)
Currently translated at 98.4% (2795 of 2840 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 98.2% (2790 of 2840 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 83.9% (2385 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/nl/
* Translated using Weblate (Hindi)
Currently translated at 9.6% (275 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hi/
* Translated using Weblate (Czech)
Currently translated at 95.4% (2710 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/
* Translated using Weblate (Ukrainian)
Currently translated at 87.2% (2477 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/
* Translated using Weblate (Finnish)
Currently translated at 50.3% (1430 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fi/
* Translated using Weblate (Korean)
Currently translated at 51.6% (1466 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ko/
* Translated using Weblate (Polish)
Currently translated at 88.7% (2520 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pl/
* Translated using Weblate (Portuguese)
Currently translated at 33.0% (939 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/pt/
* Translated using Weblate (Hebrew)
Currently translated at 72.6% (2062 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/he/
* Translated using Weblate (Thai)
Currently translated at 43.5% (1236 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/th/
* Translated using Weblate (Thai)
Currently translated at 43.5% (1236 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/th/
* Translated using Weblate (Bulgarian)
Currently translated at 87.2% (2479 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/bg/
* Translated using Weblate (Turkish)
Currently translated at 86.0% (2445 of 2840 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/tr/
* Translated using Weblate (German)
Currently translated at 100.0% (2843 of 2843 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% (2439 of 2439 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% (2843 of 2843 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 98.5% (2803 of 2843 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% (2843 of 2843 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% (2439 of 2439 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% (2843 of 2843 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 84.2% (2396 of 2843 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% (2439 of 2439 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 99.9% (2842 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/
* Translated using Weblate (Vietnamese)
Currently translated at 85.5% (2433 of 2843 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 100.0% (2843 of 2843 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% (2439 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/
* Translated using Weblate (Hindi)
Currently translated at 9.8% (281 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hi/
* Translated using Weblate (Indonesian)
Currently translated at 89.5% (2547 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/
* Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 88.1% (2149 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/
* Translated using Weblate (Russian)
Currently translated at 97.4% (2771 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/
* Translated using Weblate (French)
Currently translated at 78.9% (1926 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 89.3% (2180 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/
* Translated using Weblate (Spanish)
Currently translated at 98.1% (2789 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/
* Translated using Weblate (Japanese)
Currently translated at 49.5% (1209 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/
* Translated using Weblate (Hindi)
Currently translated at 10.2% (292 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hi/
* Translated using Weblate (Czech)
Currently translated at 95.5% (2717 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/
* Translated using Weblate (Romanian)
Currently translated at 88.3% (2511 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/
* Translated using Weblate (Vietnamese)
Currently translated at 86.1% (2449 of 2843 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 94.3% (2683 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/
* Translated using Weblate (Danish)
Currently translated at 30.7% (875 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/da/
* Translated using Weblate (Spanish)
Currently translated at 98.8% (2411 of 2439 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 89.9% (2557 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/
* Translated using Weblate (Ukrainian)
Currently translated at 87.8% (2143 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/
* Translated using Weblate (Vietnamese)
Currently translated at 87.4% (2485 of 2843 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 87.4% (2485 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/
* Translated using Weblate (Ukrainian)
Currently translated at 90.0% (2559 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/
* Translated using Weblate (Ukrainian)
Currently translated at 93.7% (2664 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/
* Translated using Weblate (Ukrainian)
Currently translated at 93.7% (2664 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/
* Translated using Weblate (Ukrainian)
Currently translated at 93.7% (2664 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/uk/
* Translated using Weblate (Ukrainian)
Currently translated at 88.0% (2148 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/uk/
* Translated using Weblate (Greek)
Currently translated at 1.0% (25 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/el/
* Translated using Weblate (Spanish)
Currently translated at 99.7% (2837 of 2843 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 99.7% (2432 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/
* Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 89.4% (2182 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/
* Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 47.0% (1148 of 2439 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 99.7% (2433 of 2439 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 88.4% (2514 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/tr/
* Translated using Weblate (Indonesian)
Currently translated at 94.4% (2686 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/
* Translated using Weblate (Russian)
Currently translated at 97.5% (2774 of 2843 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 97.6% (2382 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/
* Translated using Weblate (French)
Currently translated at 80.8% (1972 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Italian)
Currently translated at 100.0% (2439 of 2439 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% (2439 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/
* Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 98.7% (2808 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hant/
* Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (2439 of 2439 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% (2843 of 2843 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% (2843 of 2843 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% (2439 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/
* Translated using Weblate (Spanish)
Currently translated at 100.0% (2439 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/
* Translated using Weblate (Spanish)
Currently translated at 100.0% (2439 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/
* Translated using Weblate (Indonesian)
Currently translated at 94.7% (2694 of 2843 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 (German)
Currently translated at 100.0% (2439 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/
* Translated using Weblate (French)
Currently translated at 85.4% (2084 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Italian)
Currently translated at 100.0% (2439 of 2439 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% (2843 of 2843 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% (2439 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/
* Translated using Weblate (Japanese)
Currently translated at 99.0% (2815 of 2843 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ja/
* Translated using Weblate (Indonesian)
Currently translated at 95.4% (2713 of 2843 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% (2439 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/
* Translated using Weblate (French)
Currently translated at 87.5% (2136 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (French)
Currently translated at 87.5% (2136 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (French)
Currently translated at 88.0% (2147 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (French)
Currently translated at 89.2% (2178 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Italian)
Currently translated at 100.0% (2439 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/
* Translated using Weblate (German)
Currently translated at 100.0% (2866 of 2866 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% (2439 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/
* Translated using Weblate (German)
Currently translated at 100.0% (2439 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/
* Translated using Weblate (French)
Currently translated at 94.9% (2316 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
* Translated using Weblate (Italian)
Currently translated at 100.0% (2866 of 2866 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 99.9% (2865 of 2866 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/
* Translated using Weblate (Japanese)
Currently translated at 49.9% (1219 of 2439 strings)
Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/
* Translated using Weblate (Arabic)
Currently translated at 100.0% (2866 of 2866 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% (2866 of 2866 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/
* Translated using Weblate (Russian)
Currently translated at 97.9% (2807 of 2866 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 97.9% (2807 of 2866 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 97.9% (2807 of 2866 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/
* Translated using Weblate (Turkish)
Currently translated at 98.7% (2829 of 2866 strings)
Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/tr/
* process localizations
* fix android translations
* ui: fix translation errors and revert Turkish vandalism (PR #7206 review)
Corrects issues found reviewing the incoming translations across 20 languages.
CRITICAL:
- Turkish: revert 8 vandalized strings to faithful translations — the
"Open external link?" security dialog, app-upgrade prompt, relay-status
labels, and the "Privacy: for owners and subscribers" line had been
replaced with joke/offensive text.
- Ukrainian: fix a files/media privacy toggle that showed an unrelated
"chat with admins" string.
HIGH/MEDIUM: badge voice/tense inversions (uk, vi, ro, de), meaning
mistranslations (fr, es, id, vi, zh-Hans, ja, nl, ru), the Danish relay-bar
"/" separator loss, re-added role-name quotes (de, es, it, ar, ru, zh-Hans,
zh-Hant), restored dropped \n line breaks and sentences (id), and typos
(hu, es, uk, ro, cs, bg).
Placeholders, XML well-formedness, and .strings validity verified intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ui: revert residual Turkish vandalism (Owner→Emperor)
Second-pass review caught vandalized strings missed in the first fix round:
- member_info_section_title_owner: "İmparator" (Emperor) → "Sahip" (Owner)
- channel_members_section_owners: "İmparatorlar ve katkıda bulunanlar"
(Emperors and contributors) → "Sahipler ve katkıda bulunanlar"
- migrate: "Göç" (emigration) → "Taşı" (correct verb for the Migrate action)
Same vandal (mehmetksm@tuta.io) as the strings reverted in 9b5486bfa.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ui: translate romanized Hindi connect-flow strings
Second-pass reconciliation caught 6 Hindi strings left as broken "Hinglish"
(English words in Devanagari / untranslated) that were missed in the first
fix round — now proper Hindi:
- connect_via_contact_link, connect_via_invitation_link,
connect_use_current_profile, connect_use_new_incognito_profile,
connect_use_incognito_profile, profile_will_be_sent_to_contact_sending_link
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: Deleted User <noreply+6980@weblate.org>
Co-authored-by: No name <usir.alerts@onionmail.org>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: Deleted User <noreply+59251@weblate.org>
Co-authored-by: zenobit <zenobit@disroot.org>
Co-authored-by: Maksym Lukashenko <livelmaxim@gmail.com>
Co-authored-by: petri <pkajander@gmail.com>
Co-authored-by: okmepro <ix28@proton.me>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: Paulo Alexandre Pereira <github@paapereira.com>
Co-authored-by: Roee Hershberg <roihershberg@protonmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Titapa (PunPun) Chaiyakiturajai <titapapunne@gmail.com>
Co-authored-by: elgratea <weblate@fastmail.com>
Co-authored-by: xe1st <dnzkckali@gmail.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate user 54392 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: summoner001 <summoner@disroot.org>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Thu Pham-Anh <satoneken@gmail.com>
Co-authored-by: SquiralDot...🖤 <siaspride@gmail.com>
Co-authored-by: andiasriefail <andiasriefail2004@gmail.com>
Co-authored-by: Frank Yang <yangchuanxu@live.cn>
Co-authored-by: Artemchik Dornikov <artemchikdornikov@gmail.com>
Co-authored-by: cedev-1 <cedevserver@gmail.com>
Co-authored-by: cllih <a1482007736@163.com>
Co-authored-by: Aliet Expósito García <disturbed.prayer@gmail.com>
Co-authored-by: koreirozzu <trueworldfacts+asjfsajfhajfh@proton.me>
Co-authored-by: Bharadwaj <saibharadwajreddy09@gmail.com>
Co-authored-by: slrslr <adm@prnet.info>
Co-authored-by: Filip Ionut <iiiff222@gmail.com>
Co-authored-by: Nguyễn Xuân Cảm <nguyenxuancamit@gmail.com>
Co-authored-by: Dimas Alfa Pratama <world.russia.soviet.russia@gmail.com>
Co-authored-by: Mathias Pedersen <mathias.k.pedersen@hotmail.com>
Co-authored-by: kudebug <joel.hazas@outlook.es>
Co-authored-by: rider of the storm <pukhnobg@gmail.com>
Co-authored-by: Nguyen Thanh Long <longnt121004@gmail.com>
Co-authored-by: Fill Vj <screen4fill@gmail.com>
Co-authored-by: Stonepilot <lojide7802@httpsu.com>
Co-authored-by: Bugatsinho <bugatsinho@protonmail.com>
Co-authored-by: Darío Hereñú <magallania@gmail.com>
Co-authored-by: Loping151 <wangkailing151@gmail.com>
Co-authored-by: pahiy <ayazdoruck@gmail.com>
Co-authored-by: M Yusup Hamdani <dani.webdev@gmail.com>
Co-authored-by: lazizkhalilov <ssilkavtaganrog@icloud.com>
Co-authored-by: Игорь Линчевский <insrtnqme@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Irnovi Albaweny <huiralb@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: SHE LINGZHAO <rottesya@gmail.com>
Co-authored-by: Rafi <rafimuhmad90@protonmail.com>
Co-authored-by: ryokky3 <ryoxnasan@outlook.jp>
Co-authored-by: Auri <serg_sarov@mail.ru>
Co-authored-by: kkpanfilov <kkpanfilov@outlook.com>
Co-authored-by: echoloji <mehmetksm@tuta.io>
Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* website: translations (#7207)
* Translated using Weblate (French)
Currently translated at 75.3% (281 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/fr/
* Translated using Weblate (French)
Currently translated at 75.3% (281 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/fr/
* Translated using Weblate (Czech)
Currently translated at 100.0% (373 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/cs/
* Translated using Weblate (Indonesian)
Currently translated at 94.1% (351 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/id/
* Translated using Weblate (Spanish)
Currently translated at 100.0% (373 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/es/
* Translated using Weblate (French)
Currently translated at 75.3% (281 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/fr/
* Translated using Weblate (French)
Currently translated at 75.3% (281 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/fr/
* Translated using Weblate (Czech)
Currently translated at 100.0% (373 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/cs/
* Translated using Weblate (Indonesian)
Currently translated at 94.1% (351 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/id/
* Translated using Weblate (Spanish)
Currently translated at 100.0% (373 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/es/
* Translated using Weblate (Japanese)
Currently translated at 79.6% (297 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/ja/
* Translated using Weblate (German)
Currently translated at 100.0% (373 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/de/
* Translated using Weblate (Russian)
Currently translated at 87.9% (328 of 373 strings)
Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/ru/
* website: fix mistranslated security/feature terms (PR #7207 review)
- Japanese: "Repudiation" (deniability) was rendered as 否認防止
(non-repudiation, the opposite property) — corrected to 否認可能性;
"2-factor key exchange" was 2ファクタ認証 (authentication) — corrected
to 2ファクタ鍵交換.
- French: message padding ("Briar pads messages to ... bytes") was
translated as a size limit ("limite la taille") — corrected to padding
("complète les messages à une taille arrondie à ...").
- Simplified Chinese: the "Community Credits" feature name was rendered as
社区声望 (community reputation) — restored to "Community Credits".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: N4TH4NOT <n4th4not@gmail.com>
Co-authored-by: cedev-1 <cedevserver@gmail.com>
Co-authored-by: slrslr <adm@prnet.info>
Co-authored-by: huzaifah <huzaifahasif3@gmail.com>
Co-authored-by: kudebug <joel.hazas@outlook.es>
Co-authored-by: ryokky3 <ryoxnasan@outlook.jp>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Auri <serg_sarov@mail.ru>
Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* dont change font for unverified names
* better verified names view
* better layout for verified status
* shift icon
* move icon
* set domain on business chat
* change padding
* fix, test
* add params to log
* add domain to the usual group
* add domain to BusinessChatInfo
* bot types
* remove comment
* remove comment
Co-authored-by: Evgeny <evgeny@poberezkin.com>
* fix query
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: Deleted User <noreply+6980@weblate.org>
Co-authored-by: No name <usir.alerts@onionmail.org>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: Deleted User <noreply+59251@weblate.org>
Co-authored-by: zenobit <zenobit@disroot.org>
Co-authored-by: Maksym Lukashenko <livelmaxim@gmail.com>
Co-authored-by: petri <pkajander@gmail.com>
Co-authored-by: okmepro <ix28@proton.me>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: Paulo Alexandre Pereira <github@paapereira.com>
Co-authored-by: Roee Hershberg <roihershberg@protonmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Titapa (PunPun) Chaiyakiturajai <titapapunne@gmail.com>
Co-authored-by: elgratea <weblate@fastmail.com>
Co-authored-by: xe1st <dnzkckali@gmail.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate user 54392 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: summoner001 <summoner@disroot.org>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: Thu Pham-Anh <satoneken@gmail.com>
Co-authored-by: SquiralDot...🖤 <siaspride@gmail.com>
Co-authored-by: andiasriefail <andiasriefail2004@gmail.com>
Co-authored-by: Frank Yang <yangchuanxu@live.cn>
Co-authored-by: Artemchik Dornikov <artemchikdornikov@gmail.com>
Co-authored-by: cedev-1 <cedevserver@gmail.com>
Co-authored-by: cllih <a1482007736@163.com>
Co-authored-by: Aliet Expósito García <disturbed.prayer@gmail.com>
Co-authored-by: koreirozzu <trueworldfacts+asjfsajfhajfh@proton.me>
Co-authored-by: Bharadwaj <saibharadwajreddy09@gmail.com>
Co-authored-by: slrslr <adm@prnet.info>
Co-authored-by: Filip Ionut <iiiff222@gmail.com>
Co-authored-by: Nguyễn Xuân Cảm <nguyenxuancamit@gmail.com>
Co-authored-by: Dimas Alfa Pratama <world.russia.soviet.russia@gmail.com>
Co-authored-by: Mathias Pedersen <mathias.k.pedersen@hotmail.com>
Co-authored-by: kudebug <joel.hazas@outlook.es>
Co-authored-by: rider of the storm <pukhnobg@gmail.com>
Co-authored-by: Nguyen Thanh Long <longnt121004@gmail.com>
Co-authored-by: Fill Vj <screen4fill@gmail.com>
Co-authored-by: Stonepilot <lojide7802@httpsu.com>
Co-authored-by: Bugatsinho <bugatsinho@protonmail.com>
Co-authored-by: Darío Hereñú <magallania@gmail.com>
Co-authored-by: Loping151 <wangkailing151@gmail.com>
Co-authored-by: pahiy <ayazdoruck@gmail.com>
Co-authored-by: M Yusup Hamdani <dani.webdev@gmail.com>
Co-authored-by: lazizkhalilov <ssilkavtaganrog@icloud.com>
Co-authored-by: Игорь Линчевский <insrtnqme@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Irnovi Albaweny <huiralb@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: SHE LINGZHAO <rottesya@gmail.com>
Co-authored-by: Rafi <rafimuhmad90@protonmail.com>
Co-authored-by: ryokky3 <ryoxnasan@outlook.jp>
Co-authored-by: Auri <serg_sarov@mail.ru>
Co-authored-by: kkpanfilov <kkpanfilov@outlook.com>
Co-authored-by: echoloji <mehmetksm@tuta.io>
Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: N4TH4NOT <n4th4not@gmail.com>
Co-authored-by: huzaifah <huzaifahasif3@gmail.com>
1255 lines
53 KiB
Swift
1255 lines
53 KiB
Swift
//
|
|
// GroupChatInfoView.swift
|
|
// SimpleX (iOS)
|
|
//
|
|
// Created by JRoberts on 14.07.2022.
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
//
|
|
// Spec: spec/client/chat-view.md
|
|
|
|
import SwiftUI
|
|
import SimpleXChat
|
|
|
|
let SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
|
|
|
|
// Spec: spec/client/chat-view.md#GroupChatInfoView
|
|
struct GroupChatInfoView: View {
|
|
@EnvironmentObject var chatModel: ChatModel
|
|
@EnvironmentObject var theme: AppTheme
|
|
@Environment(\.dismiss) var dismiss: DismissAction
|
|
@ObservedObject var chat: Chat
|
|
@Binding var groupInfo: GroupInfo
|
|
@Binding var scrollToItemId: ChatItem.ID?
|
|
@Binding var composeState: ComposeState
|
|
var onSearch: () -> Void
|
|
@State var localAlias: String
|
|
@State private var showSharePicker = false
|
|
@FocusState private var aliasTextFieldFocused: Bool
|
|
@State private var alert: GroupChatInfoViewAlert? = nil
|
|
@State private var groupLink: GroupLink?
|
|
@State private var groupLinkMemberRole: GroupMemberRole = .member
|
|
@State private var groupLinkNavLinkActive: Bool = false
|
|
@State private var addMembersNavLinkActive: Bool = false
|
|
@State private var connectionStats: ConnectionStats?
|
|
@State private var connectionCode: String?
|
|
@State private var sendReceipts = SendReceipts.userDefault(true)
|
|
@State private var sendReceiptsUserDefault = true
|
|
@State private var progressIndicator = false
|
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
|
@State private var searchText: String = ""
|
|
@FocusState private var searchFocussed
|
|
@State private var showSecrets: Set<Int> = []
|
|
|
|
enum GroupChatInfoViewAlert: Identifiable {
|
|
case deleteGroupAlert
|
|
case clearChatAlert
|
|
case leaveGroupAlert
|
|
case cantInviteIncognitoAlert
|
|
case largeGroupReceiptsDisabled
|
|
case blockMemberAlert(mem: GroupMember)
|
|
case unblockMemberAlert(mem: GroupMember)
|
|
case blockForAllAlert(mem: GroupMember)
|
|
case unblockForAllAlert(mem: GroupMember)
|
|
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
|
|
|
var id: String {
|
|
switch self {
|
|
case .deleteGroupAlert: return "deleteGroupAlert"
|
|
case .clearChatAlert: return "clearChatAlert"
|
|
case .leaveGroupAlert: return "leaveGroupAlert"
|
|
case .cantInviteIncognitoAlert: return "cantInviteIncognitoAlert"
|
|
case .largeGroupReceiptsDisabled: return "largeGroupReceiptsDisabled"
|
|
case let .blockMemberAlert(mem): return "blockMemberAlert \(mem.groupMemberId)"
|
|
case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)"
|
|
case let .blockForAllAlert(mem): return "blockForAllAlert \(mem.groupMemberId)"
|
|
case let .unblockForAllAlert(mem): return "unblockForAllAlert \(mem.groupMemberId)"
|
|
case let .error(title, _): return "error \(title)"
|
|
}
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
let members = chatModel.groupMembers
|
|
.filter { m in let status = m.wrapped.memberStatus; return status != .memLeft && status != .memRemoved }
|
|
.sorted { $0.wrapped.memberRole > $1.wrapped.memberRole }
|
|
|
|
ZStack {
|
|
List {
|
|
groupInfoHeader()
|
|
.listRowBackground(Color.clear)
|
|
|
|
localAliasTextEdit()
|
|
.listRowBackground(Color.clear)
|
|
.listRowSeparator(.hidden)
|
|
.padding(.bottom, 18)
|
|
|
|
infoActionButtons()
|
|
.padding(.horizontal)
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: infoViewActionButtonHeight)
|
|
.listRowBackground(Color.clear)
|
|
.listRowSeparator(.hidden)
|
|
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
|
|
|
if groupInfo.useRelays && groupInfo.membership.memberIncognito {
|
|
Section(header: Text("Incognito").foregroundColor(theme.colors.secondary)) {
|
|
HStack {
|
|
Text("Your random profile")
|
|
Spacer()
|
|
Text(groupInfo.membership.chatViewName)
|
|
.foregroundStyle(.indigo)
|
|
}
|
|
}
|
|
}
|
|
|
|
let showUserSupportChat = groupInfo.membership.memberActive
|
|
&& ((groupInfo.fullGroupPreferences.support.on && groupInfo.membership.memberRole < .moderator)
|
|
|| groupInfo.membership.supportChat != nil)
|
|
|
|
if groupInfo.useRelays {
|
|
Section {
|
|
// TODO [relays] allow other owners to manage channel link (requires protocol changes to share link ownership)
|
|
if groupInfo.isOwner && groupLink != nil {
|
|
channelLinkButton()
|
|
channelSimplexNameButton()
|
|
} else if let link = groupInfo.groupProfile.publicGroup?.groupLink {
|
|
SimpleXLinkQRCode(uri: link)
|
|
Button {
|
|
showShareSheet(items: [simplexChatLink(link)])
|
|
} label: {
|
|
Label("Share link", systemImage: "square.and.arrow.up")
|
|
}
|
|
Button {
|
|
showSharePicker = true
|
|
} label: {
|
|
Label("Share via chat", systemImage: "arrowshape.turn.up.forward")
|
|
}
|
|
}
|
|
if groupInfo.isOwner || members.contains(where: { $0.wrapped.memberRole >= .owner }) {
|
|
channelMembersButton()
|
|
}
|
|
if groupInfo.membership.memberRole >= .moderator {
|
|
memberSupportButton()
|
|
}
|
|
if showUserSupportChat {
|
|
UserSupportChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId)
|
|
}
|
|
} footer: {
|
|
if !groupInfo.isOwner && groupInfo.groupProfile.publicGroup?.groupLink != nil {
|
|
Text("You can share a link or a QR code - anybody will be able to join the channel.")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
}
|
|
} else {
|
|
Section {
|
|
if groupInfo.canAddMembers && groupInfo.businessChat == nil {
|
|
groupLinkButton()
|
|
}
|
|
if groupInfo.businessChat == nil && groupInfo.membership.memberRole >= .moderator {
|
|
memberSupportButton()
|
|
}
|
|
if groupInfo.canModerate {
|
|
GroupReportsChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId)
|
|
}
|
|
if showUserSupportChat {
|
|
UserSupportChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId)
|
|
}
|
|
} header: {
|
|
Text("")
|
|
}
|
|
}
|
|
|
|
Section {
|
|
if groupInfo.isOwner && groupInfo.businessChat == nil {
|
|
editGroupButton()
|
|
}
|
|
if groupInfo.groupProfile.description != nil || (groupInfo.isOwner && groupInfo.businessChat == nil) {
|
|
addOrEditWelcomeMessage()
|
|
}
|
|
GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences)
|
|
} footer: {
|
|
let label: LocalizedStringKey = (
|
|
groupInfo.useRelays
|
|
? "Only channel owners can change channel preferences."
|
|
: groupInfo.businessChat == nil
|
|
? "Only group owners can change group preferences."
|
|
: "Only chat owners can change preferences."
|
|
)
|
|
Text(label)
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
|
|
Section {
|
|
if !groupInfo.useRelays {
|
|
if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
|
|
sendReceiptsOption()
|
|
} else {
|
|
sendReceiptsOptionDisabled()
|
|
}
|
|
}
|
|
NavigationLink {
|
|
ChatWallpaperEditorSheet(chat: chat)
|
|
} label: {
|
|
Label("Chat theme", systemImage: "photo")
|
|
}
|
|
ChatTTLOption(chat: chat, progressIndicator: $progressIndicator)
|
|
} footer: {
|
|
Text("Delete chat messages from your device.")
|
|
}
|
|
|
|
if !groupInfo.nextConnectPrepared && !groupInfo.useRelays {
|
|
Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) {
|
|
if groupInfo.canAddMembers {
|
|
if (chat.chatInfo.incognito) {
|
|
Label("Invite members", systemImage: "plus")
|
|
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
|
.onTapGesture { alert = .cantInviteIncognitoAlert }
|
|
} else {
|
|
addMembersButton()
|
|
}
|
|
}
|
|
searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary)
|
|
.padding(.leading, 8)
|
|
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
|
let filteredMembers = s == ""
|
|
? members
|
|
: members.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) }
|
|
MemberRowView(
|
|
chat: chat,
|
|
groupInfo: groupInfo,
|
|
groupMember: GMember(groupInfo.membership),
|
|
scrollToItemId: $scrollToItemId,
|
|
user: true,
|
|
alert: $alert
|
|
)
|
|
ForEach(filteredMembers) { member in
|
|
MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, scrollToItemId: $scrollToItemId, alert: $alert)
|
|
}
|
|
}
|
|
}
|
|
|
|
Section {
|
|
if groupInfo.useRelays && (groupInfo.isOwner || members.contains(where: { $0.wrapped.memberRole == .relay })) {
|
|
channelRelaysButton()
|
|
}
|
|
clearChatButton()
|
|
if groupInfo.canDelete {
|
|
deleteGroupButton()
|
|
}
|
|
if groupInfo.membership.memberCurrentOrPending {
|
|
if !groupInfo.useRelays || !groupInfo.isOwner
|
|
|| members.contains(where: { $0.wrapped.memberRole == .owner && $0.wrapped.groupMemberId != groupInfo.membership.groupMemberId }) {
|
|
leaveGroupButton()
|
|
}
|
|
}
|
|
}
|
|
|
|
if groupInfo.useRelays && groupInfo.isOwner {
|
|
Section(header: Text("Advanced options").foregroundColor(theme.colors.secondary)) {
|
|
channelWebAccessButton()
|
|
}
|
|
}
|
|
|
|
if developerTools {
|
|
Section(header: Text("For console").foregroundColor(theme.colors.secondary)) {
|
|
infoRow("Local name", chat.chatInfo.localDisplayName)
|
|
infoRow("Database ID", "\(chat.chatInfo.apiId)")
|
|
}
|
|
}
|
|
}
|
|
.modifier(ThemedBackground(grouped: true))
|
|
.navigationBarHidden(true)
|
|
.disabled(progressIndicator)
|
|
.opacity(progressIndicator ? 0.6 : 1)
|
|
|
|
if progressIndicator {
|
|
ProgressView().scaleEffect(2)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
.sheet(isPresented: $showSharePicker) {
|
|
shareChannelPicker(groupInfo: groupInfo, composeState: $composeState)
|
|
}
|
|
.alert(item: $alert) { alertItem in
|
|
switch(alertItem) {
|
|
case .deleteGroupAlert: return deleteGroupAlert()
|
|
case .clearChatAlert: return clearChatAlert()
|
|
case .leaveGroupAlert: return leaveGroupAlert()
|
|
case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert()
|
|
case .largeGroupReceiptsDisabled: return largeGroupReceiptsDisabledAlert()
|
|
case let .blockMemberAlert(mem): return blockMemberAlert(groupInfo, mem)
|
|
case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem)
|
|
case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem)
|
|
case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem)
|
|
case let .error(title, error): return mkAlert(title: title, message: error)
|
|
}
|
|
}
|
|
.onAppear {
|
|
if let currentUser = chatModel.currentUser {
|
|
sendReceiptsUserDefault = currentUser.sendRcptsSmallGroups
|
|
}
|
|
sendReceipts = SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault)
|
|
if !groupInfo.useRelays || groupInfo.isOwner {
|
|
do {
|
|
if let gLink = try apiGetGroupLink(groupInfo.groupId) {
|
|
groupLink = gLink
|
|
groupLinkMemberRole = gLink.acceptMemberRole
|
|
}
|
|
} catch let error {
|
|
logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func groupInfoHeader() -> some View {
|
|
VStack {
|
|
let cInfo = chat.chatInfo
|
|
// show actual display name, alias can be edited in this view
|
|
let displayName = (cInfo.groupInfo?.groupProfile.displayName ?? cInfo.displayName).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let fullName = cInfo.fullName.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
ChatInfoImage(chat: chat, size: 192, color: Color(uiColor: .tertiarySystemFill))
|
|
.padding(.top, 12)
|
|
.padding()
|
|
Text(displayName)
|
|
.font(.largeTitle)
|
|
.multilineTextAlignment(.center)
|
|
.lineLimit(4)
|
|
.padding(.bottom, 2)
|
|
if fullName != "" && fullName != displayName && fullName != cInfo.displayName.trimmingCharacters(in: .whitespacesAndNewlines) {
|
|
Text(cInfo.fullName)
|
|
.font(.title2)
|
|
.multilineTextAlignment(.center)
|
|
.lineLimit(3)
|
|
.padding(.bottom, 2)
|
|
}
|
|
if let descr = cInfo.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" {
|
|
let r = markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background)
|
|
msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true)
|
|
.multilineTextAlignment(.center)
|
|
.lineLimit(4)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
if let access = groupInfo.groupProfile.publicGroup?.publicGroupAccess,
|
|
let domain = access.groupDomainClaim?.shortName,
|
|
groupInfo.groupDomainVerified != nil || access.groupDomainClaim?.proof != nil {
|
|
SimplexNameView(
|
|
simplexName: "#\(domain)",
|
|
verified: groupInfo.groupDomainVerified,
|
|
verify: {
|
|
do {
|
|
let (gInfo, reason) = try await apiVerifyGroupDomain(groupInfo.groupId)
|
|
await MainActor.run {
|
|
chatModel.updateGroup(gInfo)
|
|
groupInfo = gInfo
|
|
}
|
|
return (gInfo.groupDomainVerified, reason)
|
|
} catch {
|
|
logger.error("apiVerifyGroupDomain: \(responseError(error))")
|
|
return nil
|
|
}
|
|
}
|
|
)
|
|
}
|
|
if let claim = groupInfo.businessChat?.businessDomain,
|
|
groupInfo.groupDomainVerified != nil || claim.proof != nil {
|
|
// A business presents as a contact, so the name retains its .simplex suffix. The tick comes from
|
|
// groupDomainVerified (set at connect); its domain proof is not received on the wire yet, so
|
|
// re-verification is not wired.
|
|
SimplexNameView(
|
|
simplexName: "@\(claim.domain)",
|
|
verified: groupInfo.groupDomainVerified,
|
|
verify: { nil }
|
|
)
|
|
}
|
|
if let webPage = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage,
|
|
let url = URL(string: webPage) {
|
|
Link(destination: url) {
|
|
Text(webPage)
|
|
.font(.subheadline)
|
|
.lineLimit(1)
|
|
.truncationMode(.tail)
|
|
}
|
|
}
|
|
if groupInfo.useRelays,
|
|
let count = groupInfo.groupSummary.publicMemberCount,
|
|
count > 0 {
|
|
Text(subscriberCountStr(count))
|
|
.font(.subheadline)
|
|
.foregroundColor(theme.colors.secondary)
|
|
.padding(.bottom, 2)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
}
|
|
|
|
private func localAliasTextEdit() -> some View {
|
|
TextField("Set chat name…", text: $localAlias)
|
|
.disableAutocorrection(true)
|
|
.focused($aliasTextFieldFocused)
|
|
.submitLabel(.done)
|
|
.onChange(of: aliasTextFieldFocused) { focused in
|
|
if !focused {
|
|
setGroupAlias()
|
|
}
|
|
}
|
|
.onSubmit {
|
|
setGroupAlias()
|
|
}
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
|
|
private func setGroupAlias() {
|
|
Task {
|
|
do {
|
|
if let gInfo = try await apiSetGroupAlias(groupId: chat.chatInfo.apiId, localAlias: localAlias) {
|
|
await MainActor.run {
|
|
chatModel.updateGroup(gInfo)
|
|
}
|
|
}
|
|
} catch {
|
|
logger.error("setGroupAlias error: \(responseError(error))")
|
|
}
|
|
}
|
|
}
|
|
|
|
func infoActionButtons() -> some View {
|
|
GeometryReader { g in
|
|
let buttonWidth = g.size.width / 4
|
|
HStack(alignment: .center, spacing: 8) {
|
|
searchButton(width: buttonWidth)
|
|
if groupInfo.useRelays && groupInfo.isOwner {
|
|
channelLinkActionButton(width: buttonWidth)
|
|
} else if !groupInfo.useRelays && groupInfo.canAddMembers {
|
|
addMembersActionButton(width: buttonWidth)
|
|
}
|
|
if let nextNtfMode = chat.chatInfo.nextNtfMode {
|
|
muteButton(width: buttonWidth, nextNtfMode: nextNtfMode)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
}
|
|
}
|
|
|
|
private func searchButton(width: CGFloat) -> some View {
|
|
InfoViewButton(image: "magnifyingglass", title: "search", width: width) {
|
|
dismiss()
|
|
onSearch()
|
|
}
|
|
.disabled(!groupInfo.ready || chat.chatItems.isEmpty)
|
|
}
|
|
|
|
private func addMembersActionButton(width: CGFloat) -> some View {
|
|
ZStack {
|
|
if chat.chatInfo.incognito {
|
|
InfoViewButton(image: "link.badge.plus", title: "invite", width: width) {
|
|
groupLinkNavLinkActive = true
|
|
}
|
|
|
|
NavigationLink(isActive: $groupLinkNavLinkActive) {
|
|
groupLinkDestinationView()
|
|
} label: {
|
|
EmptyView()
|
|
}
|
|
.frame(width: 1, height: 1)
|
|
.hidden()
|
|
} else {
|
|
InfoViewButton(image: "person.fill.badge.plus", title: "invite", width: width) {
|
|
addMembersNavLinkActive = true
|
|
}
|
|
|
|
NavigationLink(isActive: $addMembersNavLinkActive) {
|
|
addMembersDestinationView()
|
|
} label: {
|
|
EmptyView()
|
|
}
|
|
.frame(width: 1, height: 1)
|
|
.hidden()
|
|
}
|
|
}
|
|
.disabled(!groupInfo.ready)
|
|
}
|
|
|
|
private func muteButton(width: CGFloat, nextNtfMode: MsgFilter) -> some View {
|
|
return InfoViewButton(
|
|
image: nextNtfMode.iconFilled,
|
|
title: "\(nextNtfMode.text(mentions: true))",
|
|
width: width
|
|
) {
|
|
toggleNotifications(chat, enableNtfs: nextNtfMode)
|
|
}
|
|
.disabled(!groupInfo.ready)
|
|
}
|
|
|
|
private func channelLinkActionButton(width: CGFloat) -> some View {
|
|
ZStack {
|
|
InfoViewButton(image: "link", title: "link", width: width) {
|
|
groupLinkNavLinkActive = true
|
|
}
|
|
|
|
NavigationLink(isActive: $groupLinkNavLinkActive) {
|
|
groupLinkDestinationView()
|
|
} label: {
|
|
EmptyView()
|
|
}
|
|
.frame(width: 1, height: 1)
|
|
.hidden()
|
|
}
|
|
.disabled(!groupInfo.ready)
|
|
}
|
|
|
|
private func addMembersButton() -> some View {
|
|
let label: LocalizedStringKey = switch groupInfo.businessChat?.chatType {
|
|
case .customer: "Add team members"
|
|
case .business: "Add friends"
|
|
case .none: "Invite members"
|
|
}
|
|
return NavigationLink {
|
|
addMembersDestinationView()
|
|
} label: {
|
|
Label(label, systemImage: "plus")
|
|
}
|
|
}
|
|
|
|
private func addMembersDestinationView() -> some View {
|
|
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
|
.onAppear {
|
|
searchFocussed = false
|
|
Task {
|
|
await chatModel.loadGroupMembers(groupInfo)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct MemberRowView: View {
|
|
var chat: Chat
|
|
var groupInfo: GroupInfo
|
|
@ObservedObject var groupMember: GMember
|
|
@Binding var scrollToItemId: ChatItem.ID?
|
|
@EnvironmentObject var theme: AppTheme
|
|
var user: Bool = false
|
|
@Binding var alert: GroupChatInfoViewAlert?
|
|
|
|
var body: some View {
|
|
let member = groupMember.wrapped
|
|
let v1 = HStack{
|
|
MemberProfileImage(member, size: 38)
|
|
.padding(.trailing, 2)
|
|
// TODO server connection status
|
|
VStack(alignment: .leading) {
|
|
let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : theme.colors.onBackground)
|
|
NameWithBadge((member.verified ? memberVerifiedShield + t : t), member.nameBadge)
|
|
.lineLimit(1)
|
|
(user ? Text ("you: ") + Text(member.memberStatus.shortText) : Text(memberConnStatus(member)))
|
|
.lineLimit(1)
|
|
.font(.caption)
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
Spacer()
|
|
memberInfo(member)
|
|
}
|
|
|
|
let v = ZStack {
|
|
if user {
|
|
v1
|
|
} else {
|
|
NavigationLink {
|
|
memberInfoView()
|
|
} label: {
|
|
EmptyView()
|
|
}
|
|
.opacity(0)
|
|
v1
|
|
}
|
|
}
|
|
|
|
if user {
|
|
v
|
|
} else if groupInfo.membership.memberRole >= .moderator {
|
|
// TODO if there are more actions, refactor with lists of swipeActions
|
|
let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo)
|
|
let canRemove = member.canBeRemoved(groupInfo: groupInfo)
|
|
if canBlockForAll && canRemove {
|
|
removeSwipe(member, blockForAllSwipe(member, v))
|
|
} else if canBlockForAll {
|
|
blockForAllSwipe(member, v)
|
|
} else if canRemove {
|
|
removeSwipe(member, v)
|
|
} else {
|
|
v
|
|
}
|
|
} else {
|
|
if !member.blockedByAdmin {
|
|
blockSwipe(member, v)
|
|
} else {
|
|
v
|
|
}
|
|
}
|
|
}
|
|
|
|
private func memberInfoView() -> some View {
|
|
GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember, scrollToItemId: $scrollToItemId)
|
|
.navigationBarHidden(false)
|
|
}
|
|
|
|
private func memberConnStatus(_ member: GroupMember) -> LocalizedStringKey {
|
|
if case .failed = member.activeConn?.connStatus {
|
|
return "failed"
|
|
} else if member.activeConn?.connDisabled ?? false {
|
|
return "disabled"
|
|
} else if member.activeConn?.connInactive ?? false {
|
|
return "inactive"
|
|
} else {
|
|
return member.memberStatus.shortText
|
|
}
|
|
}
|
|
|
|
@ViewBuilder private func memberInfo(_ member: GroupMember) -> some View {
|
|
if member.blocked {
|
|
Text("blocked")
|
|
.foregroundColor(theme.colors.secondary)
|
|
} else {
|
|
let role = member.memberRole
|
|
if [.owner, .admin, .moderator, .observer].contains(role) {
|
|
Text(member.memberRole.text(isChannel: groupInfo.isChannel))
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func blockSwipe<V: View>(_ member: GroupMember, _ v: V) -> some View {
|
|
v.swipeActions(edge: .leading) {
|
|
if member.memberSettings.showMessages {
|
|
Button {
|
|
alert = .blockMemberAlert(mem: member)
|
|
} label: {
|
|
Label("Block member", systemImage: "hand.raised").foregroundColor(theme.colors.secondary)
|
|
}
|
|
} else {
|
|
Button {
|
|
alert = .unblockMemberAlert(mem: member)
|
|
} label: {
|
|
Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func blockForAllSwipe<V: View>(_ member: GroupMember, _ v: V) -> some View {
|
|
v.swipeActions(edge: .leading) {
|
|
if member.blockedByAdmin {
|
|
Button {
|
|
alert = .unblockForAllAlert(mem: member)
|
|
} label: {
|
|
Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary)
|
|
}
|
|
} else {
|
|
Button {
|
|
alert = .blockForAllAlert(mem: member)
|
|
} label: {
|
|
Label("Block for all", systemImage: "hand.raised").foregroundColor(theme.colors.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func removeSwipe<V: View>(_ member: GroupMember, _ v: V) -> some View {
|
|
v.swipeActions(edge: .trailing) {
|
|
Button(role: .destructive) {
|
|
showRemoveMemberAlert(groupInfo, member)
|
|
} label: {
|
|
Label("Remove member", systemImage: "trash")
|
|
.foregroundColor(Color.red)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var memberVerifiedShield: Text {
|
|
(Text(Image(systemName: "checkmark.shield")) + textSpace)
|
|
.font(.caption)
|
|
.baselineOffset(2)
|
|
.kerning(-2)
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
}
|
|
|
|
private func groupLinkButton() -> some View {
|
|
NavigationLink {
|
|
groupLinkDestinationView()
|
|
} label: {
|
|
if groupLink == nil {
|
|
Label("Create group link", systemImage: "link.badge.plus")
|
|
} else {
|
|
Label("Group link", systemImage: "link")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func channelLinkButton() -> some View {
|
|
NavigationLink {
|
|
groupLinkDestinationView()
|
|
} label: {
|
|
Label("Channel link", systemImage: "link")
|
|
}
|
|
}
|
|
|
|
private func channelWebAccessButton() -> some View {
|
|
let title: LocalizedStringKey = groupInfo.isChannel ? "Channel webpage" : "Group webpage"
|
|
return NavigationLink {
|
|
ChannelWebAccessView(groupInfo: $groupInfo)
|
|
.navigationBarTitle(title)
|
|
.navigationBarTitleDisplayMode(.large)
|
|
} label: {
|
|
Label(title, systemImage: "globe")
|
|
}
|
|
}
|
|
|
|
private func channelSimplexNameButton() -> some View {
|
|
NavigationLink {
|
|
let domain = if let d = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName { "#\(d)" } else { "" }
|
|
SetSimplexDomainView(
|
|
title: "SimpleX name",
|
|
footer: "Let people join via name registered with this channel link.",
|
|
prompt: "#channelname.testing",
|
|
simplexName: domain,
|
|
save: { domain in
|
|
do {
|
|
var access = groupInfo.groupProfile.publicGroup?.publicGroupAccess ?? PublicGroupAccess()
|
|
access.groupDomainClaim = domain.map { SimplexDomainClaim(domain: $0) }
|
|
let gInfo = try await apiSetPublicGroupAccess(groupInfo.groupId, access: access)
|
|
await MainActor.run {
|
|
chatModel.updateGroup(gInfo)
|
|
groupInfo = gInfo
|
|
}
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
)
|
|
} label: {
|
|
Label("SimpleX name", systemImage: "number")
|
|
}
|
|
}
|
|
|
|
private func groupLinkDestinationView() -> some View {
|
|
GroupLinkView(
|
|
groupId: groupInfo.groupId,
|
|
groupLink: $groupLink,
|
|
groupLinkMemberRole: $groupLinkMemberRole,
|
|
showTitle: false,
|
|
creatingGroup: false,
|
|
isChannel: groupInfo.useRelays,
|
|
groupInfo: groupInfo,
|
|
composeState: $composeState
|
|
)
|
|
.navigationBarTitle(groupInfo.useRelays ? "Channel link" : "Group link")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
.navigationBarTitleDisplayMode(.large)
|
|
}
|
|
|
|
private func channelMembersButton() -> some View {
|
|
let label: LocalizedStringKey = groupInfo.isOwner ? "Subscribers" : "Owners & contributors"
|
|
return NavigationLink {
|
|
ChannelMembersView(chat: chat, groupInfo: groupInfo)
|
|
.navigationTitle(label)
|
|
.modifier(ThemedBackground(grouped: true))
|
|
.navigationBarTitleDisplayMode(.large)
|
|
} label: {
|
|
Label(label, systemImage: "person.2")
|
|
}
|
|
}
|
|
|
|
private func channelRelaysButton() -> some View {
|
|
NavigationLink {
|
|
ChannelRelaysView(chat: chat, groupInfo: groupInfo)
|
|
.navigationTitle("Chat relays")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
.navigationBarTitleDisplayMode(.large)
|
|
} label: {
|
|
Label("Chat relays", systemImage: "externaldrive.connected.to.line.below")
|
|
}
|
|
}
|
|
|
|
struct UserSupportChatNavLink: View {
|
|
@ObservedObject var chat: Chat
|
|
@EnvironmentObject var theme: AppTheme
|
|
var groupInfo: GroupInfo
|
|
@EnvironmentObject var chatModel: ChatModel
|
|
@Binding var scrollToItemId: ChatItem.ID?
|
|
@State private var navLinkActive = false
|
|
|
|
var body: some View {
|
|
let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: nil)
|
|
NavigationLink(isActive: $navLinkActive) {
|
|
SecondaryChatView(
|
|
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()),
|
|
scrollToItemId: $scrollToItemId
|
|
)
|
|
} label: {
|
|
HStack {
|
|
Label("Chat with admins", systemImage: chat.supportUnreadCount > 0 ? "flag.fill" : "flag")
|
|
Spacer()
|
|
if chat.supportUnreadCount > 0 {
|
|
UnreadBadge(count: chat.supportUnreadCount, color: theme.colors.primary)
|
|
}
|
|
}
|
|
}
|
|
.onChange(of: navLinkActive) { active in
|
|
if active {
|
|
ItemsModel.loadSecondaryChat(groupInfo.id, chatFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func memberSupportButton() -> some View {
|
|
NavigationLink {
|
|
MemberSupportView(groupInfo: groupInfo, scrollToItemId: $scrollToItemId)
|
|
.navigationBarTitle("Chats with members")
|
|
.modifier(ThemedBackground())
|
|
.navigationBarTitleDisplayMode(.large)
|
|
} label: {
|
|
HStack {
|
|
Label(
|
|
"Chats with members",
|
|
systemImage: chat.supportUnreadCount > 0 ? "flag.fill" : "flag"
|
|
)
|
|
Spacer()
|
|
if chat.supportUnreadCount > 0 {
|
|
UnreadBadge(count: chat.supportUnreadCount, color: theme.colors.primary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct GroupReportsChatNavLink: View {
|
|
@ObservedObject var chat: Chat
|
|
@EnvironmentObject var theme: AppTheme
|
|
var groupInfo: GroupInfo
|
|
@EnvironmentObject var chatModel: ChatModel
|
|
@Binding var scrollToItemId: ChatItem.ID?
|
|
@State private var navLinkActive = false
|
|
|
|
var body: some View {
|
|
NavigationLink(isActive: $navLinkActive) {
|
|
SecondaryChatView(
|
|
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: .reports), chatItems: [], chatStats: ChatStats()),
|
|
scrollToItemId: $scrollToItemId
|
|
)
|
|
} label: {
|
|
HStack {
|
|
Label {
|
|
Text("Member reports")
|
|
} icon: {
|
|
Image(systemName: chat.chatStats.reportsCount > 0 ? "flag.fill" : "flag").foregroundColor(.red)
|
|
}
|
|
Spacer()
|
|
if chat.chatStats.reportsCount > 0 {
|
|
UnreadBadge(count: chat.chatStats.reportsCount, color: .red)
|
|
}
|
|
}
|
|
}
|
|
.onChange(of: navLinkActive) { active in
|
|
if active {
|
|
ItemsModel.loadSecondaryChat(chat.id, chatFilter: .msgContentTagContext(contentTag: .report))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func editGroupButton() -> some View {
|
|
NavigationLink {
|
|
GroupProfileView(
|
|
groupInfo: $groupInfo,
|
|
groupProfile: groupInfo.groupProfile
|
|
)
|
|
} label: {
|
|
Label(groupInfo.useRelays ? "Edit channel profile" : "Edit group profile", systemImage: "pencil")
|
|
}
|
|
}
|
|
|
|
private func addOrEditWelcomeMessage() -> some View {
|
|
NavigationLink {
|
|
GroupWelcomeView(
|
|
groupInfo: $groupInfo,
|
|
groupProfile: groupInfo.groupProfile,
|
|
welcomeText: groupInfo.groupProfile.description ?? ""
|
|
)
|
|
.navigationTitle("Welcome message")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
.navigationBarTitleDisplayMode(.large)
|
|
} label: {
|
|
groupInfo.groupProfile.description == nil
|
|
? Label("Add welcome message", systemImage: "plus.message")
|
|
: Label("Welcome message", systemImage: "message")
|
|
}
|
|
}
|
|
|
|
@ViewBuilder private func deleteGroupButton() -> some View {
|
|
let label: LocalizedStringKey = groupInfo.useRelays ? "Delete channel" : groupInfo.businessChat == nil ? "Delete group" : "Delete chat"
|
|
Button(role: .destructive) {
|
|
alert = .deleteGroupAlert
|
|
} label: {
|
|
Label(label, systemImage: "trash")
|
|
.foregroundColor(Color.red)
|
|
}
|
|
}
|
|
|
|
private func clearChatButton() -> some View {
|
|
Button() {
|
|
alert = .clearChatAlert
|
|
} label: {
|
|
Label("Clear conversation", systemImage: "gobackward")
|
|
.foregroundColor(Color.orange)
|
|
}
|
|
}
|
|
|
|
private func leaveGroupButton() -> some View {
|
|
let label: LocalizedStringKey = groupInfo.useRelays ? "Leave channel" : groupInfo.businessChat == nil ? "Leave group" : "Leave chat"
|
|
return Button(role: .destructive) {
|
|
alert = .leaveGroupAlert
|
|
} label: {
|
|
Label(label, systemImage: "rectangle.portrait.and.arrow.right")
|
|
.foregroundColor(Color.red)
|
|
}
|
|
}
|
|
|
|
// TODO reuse this and clearChatAlert with ChatInfoView
|
|
private func deleteGroupAlert() -> Alert {
|
|
let label: LocalizedStringKey = groupInfo.useRelays ? "Delete channel?" : groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?"
|
|
return Alert(
|
|
title: Text(label),
|
|
message: Text(chat.chatInfo.displayName + "\n\n") + deleteGroupAlertMessage(groupInfo),
|
|
primaryButton: .destructive(Text("Delete")) {
|
|
Task {
|
|
do {
|
|
try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId)
|
|
await MainActor.run {
|
|
dismiss()
|
|
chatModel.chatId = nil
|
|
chatModel.removeChat(chat.chatInfo.id)
|
|
}
|
|
} catch let error {
|
|
logger.error("deleteGroupAlert apiDeleteChat error: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
},
|
|
secondaryButton: .cancel()
|
|
)
|
|
}
|
|
|
|
private func clearChatAlert() -> Alert {
|
|
Alert(
|
|
title: Text("Clear conversation?"),
|
|
message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
|
|
primaryButton: .destructive(Text("Clear")) {
|
|
Task {
|
|
await clearChat(chat)
|
|
await MainActor.run { dismiss() }
|
|
}
|
|
},
|
|
secondaryButton: .cancel()
|
|
)
|
|
}
|
|
|
|
private func leaveGroupAlert() -> Alert {
|
|
let titleLabel: LocalizedStringKey = groupInfo.useRelays ? "Leave channel?" : groupInfo.businessChat == nil ? "Leave group?" : "Leave chat?"
|
|
let messageLabel: LocalizedStringKey = (
|
|
groupInfo.useRelays
|
|
? "You will stop receiving messages from this channel. Chat history will be preserved."
|
|
: groupInfo.businessChat == nil
|
|
? "You will stop receiving messages from this group. Chat history will be preserved."
|
|
: "You will stop receiving messages from this chat. Chat history will be preserved."
|
|
)
|
|
return Alert(
|
|
title: Text(titleLabel),
|
|
message: Text(chat.chatInfo.displayName + "\n\n") + Text(messageLabel),
|
|
primaryButton: .destructive(Text("Leave")) {
|
|
Task {
|
|
await leaveGroup(chat.chatInfo.apiId)
|
|
await MainActor.run { dismiss() }
|
|
}
|
|
},
|
|
secondaryButton: .cancel()
|
|
)
|
|
}
|
|
|
|
private func sendReceiptsOption() -> some View {
|
|
WrappedPicker(selection: $sendReceipts) {
|
|
ForEach([.yes, .no, .userDefault(sendReceiptsUserDefault)]) { (opt: SendReceipts) in
|
|
Text(opt.text)
|
|
}
|
|
} label: {
|
|
Label("Send receipts", systemImage: "checkmark.message")
|
|
}
|
|
.onChange(of: sendReceipts) { _ in
|
|
setSendReceipts()
|
|
}
|
|
}
|
|
|
|
private func setSendReceipts() {
|
|
var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults
|
|
chatSettings.sendRcpts = sendReceipts.bool()
|
|
updateChatSettings(chat, chatSettings: chatSettings)
|
|
}
|
|
|
|
private func sendReceiptsOptionDisabled() -> some View {
|
|
HStack {
|
|
Label("Send receipts", systemImage: "checkmark.message")
|
|
Spacer()
|
|
Text("disabled")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.onTapGesture {
|
|
alert = .largeGroupReceiptsDisabled
|
|
}
|
|
}
|
|
}
|
|
|
|
func showRemoveMemberAlert(_ groupInfo: GroupInfo, _ mem: GroupMember, dismiss: DismissAction? = nil) {
|
|
if mem.memberRole == .relay {
|
|
let isLastActive = groupInfo.useRelays && mem.memberCurrent && {
|
|
let activeRelays = ChatModel.shared.groupMembers.filter { $0.wrapped.memberRole == .relay && $0.wrapped.memberCurrent }
|
|
return activeRelays.count <= 1
|
|
}()
|
|
showAlert(
|
|
NSLocalizedString("Remove relay?", comment: "alert title"),
|
|
message: isLastActive
|
|
? NSLocalizedString("This is the last active relay. Removing it will prevent message delivery to subscribers.", comment: "alert message")
|
|
: NSLocalizedString("Relay will be removed from channel - this cannot be undone!", comment: "alert message"),
|
|
actions: {[
|
|
UIAlertAction(title: NSLocalizedString("Remove", comment: "alert action"), style: .destructive) { _ in
|
|
removeMember(groupInfo, mem, withMessages: false, dismiss: dismiss)
|
|
},
|
|
cancelAlertAction
|
|
]}
|
|
)
|
|
} else if groupInfo.useRelays {
|
|
showAlert(
|
|
NSLocalizedString("Remove subscriber?", comment: "alert title"),
|
|
message: NSLocalizedString("Subscriber will be removed from channel - this cannot be undone!", comment: "alert message"),
|
|
actions: {[
|
|
UIAlertAction(title: NSLocalizedString("Remove", comment: "alert action"), style: .destructive) { _ in
|
|
removeMember(groupInfo, mem, withMessages: false, dismiss: dismiss)
|
|
},
|
|
UIAlertAction(title: NSLocalizedString("Remove and delete messages", comment: "alert action"), style: .destructive) { _ in
|
|
removeMember(groupInfo, mem, withMessages: true, dismiss: dismiss)
|
|
},
|
|
cancelAlertAction
|
|
]}
|
|
)
|
|
} else {
|
|
showAlert(
|
|
NSLocalizedString("Remove member?", comment: "alert title"),
|
|
message: groupInfo.businessChat == nil
|
|
? NSLocalizedString("Member will be removed from group - this cannot be undone!", comment: "alert message")
|
|
: NSLocalizedString("Member will be removed from chat - this cannot be undone!", comment: "alert message"),
|
|
actions: {[
|
|
UIAlertAction(title: NSLocalizedString("Remove", comment: "alert action"), style: .destructive) { _ in
|
|
removeMember(groupInfo, mem, withMessages: false, dismiss: dismiss)
|
|
},
|
|
UIAlertAction(title: NSLocalizedString("Remove and delete messages", comment: "alert action"), style: .destructive) { _ in
|
|
removeMember(groupInfo, mem, withMessages: true, dismiss: dismiss)
|
|
},
|
|
cancelAlertAction
|
|
]}
|
|
)
|
|
}
|
|
}
|
|
|
|
func removeMember(_ groupInfo: GroupInfo, _ mem: GroupMember, withMessages: Bool, dismiss: DismissAction?) {
|
|
Task {
|
|
do {
|
|
let (updatedGroupInfo, updatedMembers) = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId], withMessages)
|
|
await MainActor.run {
|
|
ChatModel.shared.updateGroup(updatedGroupInfo)
|
|
updatedMembers.forEach { updatedMember in
|
|
_ = ChatModel.shared.upsertGroupMember(updatedGroupInfo, updatedMember)
|
|
if withMessages {
|
|
ChatModel.shared.removeMemberItems(updatedMember, byMember: groupInfo.membership, groupInfo)
|
|
}
|
|
}
|
|
dismiss?()
|
|
}
|
|
} catch let error {
|
|
logger.error("apiRemoveMembers error: \(responseError(error))")
|
|
await MainActor.run {
|
|
showAlert(
|
|
NSLocalizedString("Error removing member", comment: "alert title"),
|
|
message: responseError(error)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text {
|
|
groupInfo.useRelays ? (
|
|
groupInfo.membership.memberCurrent
|
|
? Text("Channel will be deleted for all subscribers - this cannot be undone!")
|
|
: Text("Channel will be deleted for you - this cannot be undone!")
|
|
) : groupInfo.businessChat == nil ? (
|
|
groupInfo.membership.memberCurrent
|
|
? Text("Group will be deleted for all members - this cannot be undone!")
|
|
: Text("Group will be deleted for you - this cannot be undone!")
|
|
) : (
|
|
groupInfo.membership.memberCurrent
|
|
? Text("Chat will be deleted for all members - this cannot be undone!")
|
|
: Text("Chat will be deleted for you - this cannot be undone!")
|
|
)
|
|
}
|
|
|
|
struct GroupPreferencesButton: View {
|
|
@Binding var groupInfo: GroupInfo
|
|
@State var preferences: FullGroupPreferences
|
|
@State var currentPreferences: FullGroupPreferences
|
|
var creatingGroup: Bool = false
|
|
|
|
private var label: LocalizedStringKey {
|
|
groupInfo.useRelays ? "Channel preferences"
|
|
: groupInfo.businessChat == nil ? "Group preferences"
|
|
: "Chat preferences"
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationLink {
|
|
GroupPreferencesView(
|
|
groupInfo: $groupInfo,
|
|
preferences: $preferences,
|
|
currentPreferences: currentPreferences,
|
|
creatingGroup: creatingGroup,
|
|
savePreferences: savePreferences
|
|
)
|
|
.navigationBarTitle(label)
|
|
.modifier(ThemedBackground(grouped: true))
|
|
.navigationBarTitleDisplayMode(.large)
|
|
.onDisappear {
|
|
let saveText = NSLocalizedString(
|
|
creatingGroup ? "Save"
|
|
: groupInfo.useRelays ? "Save and notify subscribers"
|
|
: "Save and notify group members",
|
|
comment: "alert button"
|
|
)
|
|
|
|
if groupInfo.fullGroupPreferences != preferences {
|
|
showAlert(
|
|
title: NSLocalizedString("Save preferences?", comment: "alert title"),
|
|
buttonTitle: saveText,
|
|
buttonAction: { savePreferences() },
|
|
cancelButton: true
|
|
)
|
|
}
|
|
}
|
|
} label: {
|
|
if creatingGroup {
|
|
Text("Set group preferences")
|
|
} else {
|
|
Label(label, systemImage: "switch.2")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func savePreferences() {
|
|
Task {
|
|
do {
|
|
var gp = groupInfo.groupProfile
|
|
gp.groupPreferences = toGroupPreferences(preferences)
|
|
let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp)
|
|
await MainActor.run {
|
|
groupInfo = gInfo
|
|
ChatModel.shared.updateGroup(gInfo)
|
|
currentPreferences = preferences
|
|
}
|
|
} catch {
|
|
logger.error("GroupPreferencesView apiUpdateGroup error: \(responseError(error))")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
func cantInviteIncognitoAlert() -> Alert {
|
|
Alert(
|
|
title: Text("Can't invite contacts!"),
|
|
message: Text("You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed")
|
|
)
|
|
}
|
|
|
|
func largeGroupReceiptsDisabledAlert() -> Alert {
|
|
Alert(
|
|
title: Text("Receipts are disabled"),
|
|
message: Text("This group has over \(SMALL_GROUPS_RCPS_MEM_LIMIT) members, delivery receipts are not sent.")
|
|
)
|
|
}
|
|
|
|
@ViewBuilder
|
|
func shareChannelPicker(groupInfo: GroupInfo, composeState: Binding<ComposeState>? = nil) -> some View {
|
|
let v = ChatItemForwardingView(
|
|
title: "Share channel",
|
|
isProhibited: { $0.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) },
|
|
onSelectChat: { chat in shareChatLink(chat, sourceGroupInfo: groupInfo, composeState: composeState) },
|
|
includeLocal: false
|
|
)
|
|
if #available(iOS 16.0, *) {
|
|
v.presentationDetents([.fraction(0.8)])
|
|
} else {
|
|
v
|
|
}
|
|
}
|
|
|
|
func shareChatLink(_ destChat: Chat, sourceGroupInfo: GroupInfo, composeState: Binding<ComposeState>? = nil) {
|
|
let sendAsGroup = if let gInfo = destChat.chatInfo.groupInfo { gInfo.useRelays && gInfo.membership.memberRole >= .owner } else { false }
|
|
Task {
|
|
do {
|
|
let mc = try await apiShareChatMsgContent(
|
|
shareChatType: .group, shareChatId: Int64(sourceGroupInfo.groupId),
|
|
toChatType: destChat.chatInfo.chatType, toChatId: destChat.chatInfo.apiId,
|
|
toScope: destChat.chatInfo.groupChatScope(), sendAsGroup: sendAsGroup
|
|
)
|
|
if case let .chat(_, chatLink, ownerSig) = mc {
|
|
await MainActor.run {
|
|
dismissAllSheets {
|
|
let cs = ComposeState(preview: .chatLinkPreview(chatLink: chatLink, ownerSig: ownerSig))
|
|
if let composeState {
|
|
composeState.wrappedValue = cs
|
|
} else {
|
|
ChatModel.shared.draft = cs
|
|
ChatModel.shared.draftChatId = destChat.id
|
|
}
|
|
if destChat.id != ChatModel.shared.chatId {
|
|
ItemsModel.shared.loadOpenChat(destChat.id)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
logger.error("shareChatLink: unexpected MsgContent: \(String(describing: mc))")
|
|
await MainActor.run {
|
|
showAlert(NSLocalizedString("Error sharing channel", comment: "alert title"), message: String(describing: mc))
|
|
}
|
|
}
|
|
} catch {
|
|
logger.error("shareChatLink error: \(error.localizedDescription)")
|
|
await MainActor.run {
|
|
showAlert(NSLocalizedString("Error sharing channel", comment: "alert title"), message: error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct GroupChatInfoView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
GroupChatInfoView(
|
|
chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []),
|
|
groupInfo: Binding.constant(GroupInfo.sampleData),
|
|
scrollToItemId: Binding.constant(nil),
|
|
composeState: Binding.constant(ComposeState()),
|
|
onSearch: {},
|
|
localAlias: ""
|
|
)
|
|
}
|
|
}
|