From 9b3c2e6b9d4ca7df5d47429aa939ce4a2fa45a64 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:02:24 +0000 Subject: [PATCH 01/11] github/workflows: fix reproduce-schedule (#6127) --- .github/workflows/reproduce-schedule.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reproduce-schedule.yml b/.github/workflows/reproduce-schedule.yml index e86a471782..7d28d6f70c 100644 --- a/.github/workflows/reproduce-schedule.yml +++ b/.github/workflows/reproduce-schedule.yml @@ -33,7 +33,7 @@ jobs: user: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_USER }} pass: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_PASS }} run: | - if [ -f "${GITHUB_WORKSPACE}/$TAG/_sha256sums" ]; then + if [ -f "${GITHUB_WORKSPACE}/${TAG}-simplex-chat/_sha256sums" ]; then exit 0 else curl --proto '=https' --tlsv1.2 -sSf \ From 4f016ff509a6a714b9c97d0b339c319a22f10744 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Mon, 4 Aug 2025 07:56:21 +0000 Subject: [PATCH 02/11] flatpak: update metainfo (#6150) --- scripts/flatpak/chat.simplex.simplex.metainfo.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index 9ced131207..5480e169b4 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,18 @@ + + https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html + +

New in v6.4.2:

+
    +
  • manually accept member contact requests, with option to auto-accept.
  • +
  • ignore contact requests from blocked group members.
  • +
  • markdown (links etc.) in profile bio/group purpose.
  • +
  • Linux app builds for aarch64 CPUs
  • +
+
+
https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html From 7fd15832e315c5a4b276212f376712dfde33c776 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 4 Aug 2025 14:06:13 +0100 Subject: [PATCH 03/11] readme: update group links (#6153) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1aa3b649a0..dc4b9658f7 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,9 @@ 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=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FiBkJE72asZX1NUZaYFIeKRVk6oVjb-iv%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAinqu3j74AMjODLoIRR487ZW6ysip_dlpD6Zxk18SPFY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22groupLinkId%22%3A%223wAFGCLygQHR5AwynZOHlQ%3D%3D%22%7D) +You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://smp4.simplex.im/g#hr4lvFeBmndWMKTwqiodPz3VBo_6UmdGWocXd1SupsM) -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: +There is also a group [#simplex-devs](https://smp6.simplex.im/g#Drx3efC-n418AuSpzTspw9SER0iJwrQTmKBafQHwkKM) for developers who build on SimpleX platform: - chat bots and automations - integrations with other apps @@ -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-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). +[\#SimpleX-DE](https://smp6.simplex.im/g#V6tQ-lJqsdgJJdJiLPtP326oQFKHvwinIbgruZ9K2oU) (German-speaking), [\#SimpleX-ES](https://smp5.simplex.im/g#xJ5kwDLq2305O5FmpUzvgRIXXAcAJ9S5BItCd2Wmloc) (Spanish-speaking), [\#SimpleX-FR](https://smp6.simplex.im/g#cVOpB0CKd6hEf2aWQ6sJ22E2DVgQLtdHoiSdKxXeKqk) (French-speaking), [\#SimpleX-RU](https://smp5.simplex.im/g#vwXRdfG5SgtaG6aVcITiUGd--Ux0rY1IuH4QXYxlq3U) (Russian-speaking), [\#SimpleX-IT](https://smp5.simplex.im/g#BtRcjsl29ULFNBSE2OPhp1UwZfW7PW9gUYFQTKHdjqU) (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. From 39a1c94ebca2005adeb77315185ab0a2c4203767 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 5 Aug 2025 12:50:16 +0100 Subject: [PATCH 04/11] core: updated simplexmq (prevent connection error with incomplete addresses of pre-configured servers) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cabal.project b/cabal.project index c1d83da219..d12d4fbd25 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: db325cb81f77652471a27f4331143982739f9f10 + tag: 79ba60e3ad415f07568151f6b7756f721212502c source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 8c24afe716..bb78f288c4 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."db325cb81f77652471a27f4331143982739f9f10" = "15q2zjcsmp40c65d9wgngjk7vs753nq9cxf5vd996ns1lkqq1ckb"; + "https://github.com/simplex-chat/simplexmq.git"."79ba60e3ad415f07568151f6b7756f721212502c" = "1i8gw8fi9xslk7zgnl3xggwvqg5fdscpcfnjvf78d343nbq4c30k"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; From 95f31031c4646fed7cad56c3b30e4d49979b4262 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 5 Aug 2025 16:12:49 +0100 Subject: [PATCH 05/11] docs: update join the team --- docs/JOIN_TEAM.md | 56 ++++++++++------------------------------------- 1 file changed, 11 insertions(+), 45 deletions(-) diff --git a/docs/JOIN_TEAM.md b/docs/JOIN_TEAM.md index c72a75cfec..2e428409df 100644 --- a/docs/JOIN_TEAM.md +++ b/docs/JOIN_TEAM.md @@ -6,58 +6,24 @@ layout: layouts/jobs.html # Join SimpleX Chat team -SimpleX Chat Ltd is a seed stage startup with a lot of user growth in 2022-2023, and a lot of exciting technical and product problems to solve to grow faster. +SimpleX Chat is a seed stage startup with a lot of user growth in 2022-2025, and a lot of exciting technical and product problems to solve to grow faster. -We currently have 6 full-time people in the team. +We currently have 4 people in the team. -We want to add 2 people to the team. +We are looking for passionate and creative people to help us! ## Who we are looking for -### Web designer & developer for a website contract +### Mobile application developer -You will work with the founder and a product marketing expert to convert the stories we want to tell our current and prospective users into interactive experiences. +You: +- created mobile applications for Android platforms as **your own full-time or side projects**, +- expertise with Android APIs, Kotlin and JetPack Compose framework, +- [a good taste](https://paulgraham.com/taste.html) for mobile apps design would be a bonus. -You are an expert in creating interactive web experiences: -- 15+ years of web development and design experience. -- Passionate about communications, privacy and data ownership. -- Competent using PhotoShop, 3D modelling, etc. -- Competent in Web tech, including JavaScript, animations, etc. +It is not a full time job yet, we have some specific problems to solve in the Android app. If we are happy working together it is likely to evolve into a full-time job offer in 2026. -We will NOT consider agencies or groups – it must be one person working on the project. - -### Application Haskell engineer - -You will work with the Haskell core of the client applications and with the network servers. - -You are an expert in language models, databases and Haskell: -- expert knowledge of SQL. -- Haskell strictness, exceptions, [concurrency](https://simonmar.github.io/pages/pcph.html), STM, [type systems](https://thinkingwithtypes.com). -- 15y+ of software engineering experience in complex projects. -- deep understanding of the common programming principles: - - data structures, bits and bytes, text encoding. - - [functional software design](https://mitp-content-server.mit.edu/books/content/sectbyfn/books_pres_0/6515/sicp.zip/index.html) and algorithms. - - protocols and networking. - -## About you - -- **Passionate about joining SimpleX Chat team**: - - already use SimpleX Chat to communicate with friends/family or participate in public SimpleX Chat groups. - - passionate about privacy, security and communications. - - interested to make contributions to SimpleX Chat open-source project in your free time before we hire you, as an extended test. - - you founded (and probably failed) at least one startup, or spent more time working for yourself than being employed. - -- **Exceptionally pragmatic, very fast and customer-focussed**: - - care about the customers (aka users) and about the product we build much more than about the code quality, technology stack, etc. - - believe that the simplest solution is the best. - - 2-3x faster than the most competent people you worked with. - - focus on solving only today's problems and resist engineering for the future (aka over-engineering) – see [The Duct Tape Programmer](https://www.joelonsoftware.com/2009/09/23/the-duct-tape-programmer/) and [Why I Hate Frameworks](https://medium.com/@johnfliu/why-i-hate-frameworks-6af8cbadba42). - - do not suffer from "not invented here" syndrome, at the same time interested to design and implement protocols and systems from the ground up when appropriate. - -- **Want to join a very early stage startup**: - - high pace and intensity, longer hours. - - a substantial part of the compensation is stock options. - - full transparency - we believe that too much [autonomy](https://twitter.com/KentBeck/status/851459129830850561) hurts learning and slows down progress. +Please ONLY apply if you created and released your own apps (not as a job or contract for somebody else). ## How to join the team @@ -65,6 +31,6 @@ You are an expert in language models, databases and Haskell: 2. Also look through [GitHub issues](https://github.com/simplex-chat/simplex-chat/issues) submitted by the users to see what would you want to contribute as a test. -3. [Connect to us](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FKBCmxJ3-lEjpWLPPkI6OWPk-YJneU5uY%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEAtixHJWDXvYWcoe-77vIfjvI6XWEuzUsapMS9nVHP_Go%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) via SimpleX Chat to chat about what you want to contribute and about joining the team. +3. [Connect to us](https://smp4.simplex.im/a#IWCurmcnKDvfOzGrQdqlXjKinqkvO10a2q__nWBVG6c) via SimpleX Chat to chat about what you want to contribute and about joining the team. 4. You can also email [jobs@simplex.chat](mailto:jobs@simplex.chat?subject=Join%20SimpleX%20Chat%20team) From 9596029c3094c6dae96cc9e03682d1ee31441856 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Thu, 7 Aug 2025 06:43:00 +0000 Subject: [PATCH 06/11] scripts/simplex-chat-reproduce-builds.sh: adjust to x86_64 (#6154) --- scripts/simplex-chat-reproduce-builds.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/simplex-chat-reproduce-builds.sh b/scripts/simplex-chat-reproduce-builds.sh index f66edb7043..c7d9170c4a 100755 --- a/scripts/simplex-chat-reproduce-builds.sh +++ b/scripts/simplex-chat-reproduce-builds.sh @@ -38,7 +38,7 @@ git -C "${tempdir}" clone "${repo}.git" &&\ for os in '22.04' '24.04'; do os_url="$(printf '%s' "${os}" | tr '.' '_')" - cli_name="simplex-chat-ubuntu-${os_url}-x86-64" + cli_name="simplex-chat-ubuntu-${os_url}-x86_64" deb_name="simplex-desktop-ubuntu-${os_url}-x86_64.deb" appimage_name="simplex-desktop-x86_64.AppImage" @@ -89,7 +89,7 @@ for os in '22.04' '24.04'; do # Copy deb docker cp \ - "${container_name}":/project/apps/multiplatform/release/main/deb/simplex_amd64.deb \ + "${container_name}":/project/apps/multiplatform/release/main/deb/simplex_x86_64.deb \ "${init_dir}/${TAG}-${repo_name}/from-source/${deb_name}" # Download prebuilt deb package From 4811d663e61d980e18ccd22f49b7f2d50fb2b81b Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 7 Aug 2025 11:13:35 +0100 Subject: [PATCH 07/11] rfc: bot messages and buttons, core: command markdown, supported commands in profile preferences, chat sessions preference, peer type field in profile to identify bots (#5360) * rfc: bot messages and buttons * update * update bot rfc * core: add bot commands to chat preferences and peer type to profile * update postgresql schema * update query plans * chat sessions preference * markdown for bot commands * schema * core: file preference, options to create bot from CLI * core: different command type * ios: commands menu * update types * update ios * improve command markdown * core, ios: update types * android, desktop: clickable commands in messages in chats with bots * android, desktop: commands menu * command menu button, bot icon * ios: connect flow for bots * android, desktop: connect flow for bots * icon * CLI commands to view and set commands, remove "hidden" property of command, bot api docs * corrections * fix inheriting profile preferences to business groups * note on business address * ios: export localizations * fix test * commands to set file preference on user/contact, tidy up layout and display of command and attachment buttons --- .../Views/Chat/ChatItem/MsgContentView.swift | 49 +++- apps/ios/Shared/Views/Chat/ChatView.swift | 13 +- .../Shared/Views/Chat/CommandsMenuView.swift | 187 ++++++++++++++ .../Chat/ComposeMessage/ComposeView.swift | 121 +++++---- .../Views/Chat/Group/GroupMentions.swift | 2 +- .../Views/ChatList/ChatPreviewView.swift | 4 +- .../Shared/Views/Helpers/ProfileImage.swift | 1 + .../Shared/Views/NewChat/NewChatView.swift | 10 +- .../bg.xcloc/Localized Contents/bg.xliff | 42 ++- .../cs.xcloc/Localized Contents/cs.xliff | 42 ++- .../de.xcloc/Localized Contents/de.xliff | 42 ++- .../en.xcloc/Localized Contents/en.xliff | 52 +++- .../es.xcloc/Localized Contents/es.xliff | 42 ++- .../fi.xcloc/Localized Contents/fi.xliff | 42 ++- .../fr.xcloc/Localized Contents/fr.xliff | 42 ++- .../hu.xcloc/Localized Contents/hu.xliff | 42 ++- .../it.xcloc/Localized Contents/it.xliff | 42 ++- .../ja.xcloc/Localized Contents/ja.xliff | 42 ++- .../nl.xcloc/Localized Contents/nl.xliff | 42 ++- .../pl.xcloc/Localized Contents/pl.xliff | 42 ++- .../ru.xcloc/Localized Contents/ru.xliff | 42 ++- .../th.xcloc/Localized Contents/th.xliff | 42 ++- .../tr.xcloc/Localized Contents/tr.xliff | 42 ++- .../uk.xcloc/Localized Contents/uk.xliff | 42 ++- .../Localized Contents/zh-Hans.xliff | 42 ++- apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 + apps/ios/SimpleXChat/ChatTypes.swift | 230 +++++++++++++++-- apps/ios/SimpleXChat/ChatUtils.swift | 2 +- .../chat/simplex/common/model/ChatModel.kt | 67 ++++- .../chat/simplex/common/model/SimpleXAPI.kt | 64 ++++- .../simplex/common/views/chat/ChatView.kt | 80 +++--- .../common/views/chat/CommandsMenuView.kt | 244 ++++++++++++++++++ .../simplex/common/views/chat/ComposeView.kt | 65 ++++- .../views/chat/SelectableChatItemToolbars.kt | 2 +- .../views/chat/group/GroupMembersToolbar.kt | 2 +- .../common/views/chat/item/ChatItemView.kt | 9 +- .../common/views/chat/item/FramedItemView.kt | 53 +++- .../common/views/chat/item/TextItemView.kt | 75 +++--- .../common/views/chatlist/ChatPreviewView.kt | 4 +- .../common/views/helpers/AnimationUtils.kt | 12 +- .../common/views/helpers/ChatInfoImage.kt | 1 + .../common/views/newchat/ConnectPlan.kt | 9 +- .../commonMain/resources/MR/base/strings.xml | 11 + .../resources/MR/images/ic_cube.svg | 1 + .../src/Broadcast/Options.hs | 13 +- .../src/Directory/Options.hs | 7 +- bots/README.md | 64 ++++- bots/api/COMMANDS.md | 145 ++++++++++- bots/api/TYPES.md | 61 +++++ bots/src/API/Docs/Commands.hs | 18 +- bots/src/API/Docs/Responses.hs | 12 +- bots/src/API/Docs/Types.hs | 7 +- bots/src/API/TypeInfo.hs | 15 +- docs/protocol/simplex-chat.schema.json | 74 +++++- docs/rfcs/2024-11-28-business-address.md | 2 +- docs/rfcs/2024-12-08-chat-bot-ui.md | 54 ++++ simplex-chat.cabal | 2 + src/Simplex/Chat/Controller.hs | 1 + src/Simplex/Chat/Core.hs | 36 ++- src/Simplex/Chat/Library/Commands.hs | 36 ++- src/Simplex/Chat/Library/Internal.hs | 4 +- src/Simplex/Chat/Markdown.hs | 42 ++- src/Simplex/Chat/Mobile.hs | 1 + src/Simplex/Chat/Options.hs | 24 ++ src/Simplex/Chat/ProfileGenerator.hs | 2 +- src/Simplex/Chat/Store/Connections.hs | 10 +- src/Simplex/Chat/Store/ContactRequest.hs | 7 +- src/Simplex/Chat/Store/Direct.hs | 12 +- src/Simplex/Chat/Store/Groups.hs | 14 +- src/Simplex/Chat/Store/Messages.hs | 10 +- src/Simplex/Chat/Store/Postgres/Migrations.hs | 4 +- .../Migrations/M20250802_chat_peer_type.hs | 21 ++ .../Store/Postgres/Migrations/chat_schema.sql | 3 +- src/Simplex/Chat/Store/Profiles.hs | 14 +- src/Simplex/Chat/Store/SQLite/Migrations.hs | 4 +- .../Migrations/M20250802_chat_peer_type.hs | 18 ++ .../SQLite/Migrations/chat_query_plans.txt | 90 +++---- .../Store/SQLite/Migrations/chat_schema.sql | 3 +- src/Simplex/Chat/Store/Shared.hs | 36 +-- src/Simplex/Chat/Types.hs | 46 +++- src/Simplex/Chat/Types/Preferences.hs | 199 ++++++++++++-- src/Simplex/Chat/View.hs | 30 ++- tests/Bots/BroadcastTests.hs | 5 +- tests/Bots/DirectoryTests.hs | 4 +- tests/ChatClient.hs | 1 + tests/ChatTests/Local.hs | 1 - tests/ChatTests/Profiles.hs | 24 +- tests/ChatTests/Utils.hs | 3 +- tests/JSONFixtures.hs | 6 +- tests/MarkdownTests.hs | 31 +++ tests/ProtocolTests.hs | 8 +- tests/RemoteTests.hs | 4 - tests/Test.hs | 2 +- 93 files changed, 2810 insertions(+), 466 deletions(-) create mode 100644 apps/ios/Shared/Views/Chat/CommandsMenuView.swift create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/CommandsMenuView.kt create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cube.svg create mode 100644 docs/rfcs/2024-12-08-chat-bot-ui.md create mode 100644 src/Simplex/Chat/Store/Postgres/Migrations/M20250802_chat_peer_type.hs create mode 100644 src/Simplex/Chat/Store/SQLite/Migrations/M20250802_chat_peer_type.hs diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 30ed3fa1a4..e743e0bffa 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -93,7 +93,18 @@ struct MsgContentView: View { @inline(__always) private func msgContentView() -> some View { - let r = messageText(text, formattedText, textStyle: textStyle, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: containerBackground, prefix: prefix) + let r = messageText( + text, + formattedText, + textStyle: textStyle, + sender: sender, + mentions: mentions, + userMemberId: userMemberId, + showSecrets: showSecrets, + commands: chat.chatInfo.useCommands && chat.chatInfo.sndReady, + backgroundColor: containerBackground, + prefix: prefix + ) let s = r.string let t: Text if let mt = meta { @@ -104,7 +115,7 @@ struct MsgContentView: View { } else { t = Text(AttributedString(s)) } - return msgTextResultView(r, t, showSecrets: $showSecrets) + return msgTextResultView(r, t, showSecrets: $showSecrets, sendCommand: { cmd in sendCommandMsg(chat, cmd) }) } @inline(__always) @@ -120,14 +131,27 @@ struct MsgContentView: View { } } -func msgTextResultView(_ r: MsgTextResult, _ t: Text, showSecrets: Binding>? = nil, centered: Bool = false, smallFont: Bool = false) -> some View { +func msgTextResultView( + _ r: MsgTextResult, + _ t: Text, + showSecrets: Binding>? = nil, + sendCommand: ((String) -> Void)? = nil, + centered: Bool = false, + smallFont: Bool = false +) -> some View { t.if(r.hasSecrets, transform: hiddenSecretsView) - .if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets, centered: centered, smallFont: smallFont)) } + .if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets, sendCommand: sendCommand, centered: centered, smallFont: smallFont)) } } // smallFont parameter is used to pad height, otherwise CTFrameGetLines fails to see them as lines - it's needed if font is not .body @inline(__always) -private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding>? = nil, centered: Bool, smallFont: Bool) -> some View { +private func handleTextTaps( + _ s: NSAttributedString, + showSecrets: Binding>? = nil, + sendCommand: ((String) -> Void)? = nil, + centered: Bool, + smallFont: Bool +) -> some View { return GeometryReader { g in Rectangle() .fill(Color.clear) @@ -187,6 +211,8 @@ private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding?, + commands: Bool = false, backgroundColor: UIColor, prefix: NSAttributedString? = nil ) -> MsgTextResult { @@ -343,6 +373,15 @@ func messageText( if case .description = privacySimplexLinkModeDefault.get() { t = simplexLinkText(linkType, smpHosts) } + case let .command(cmdStr): + snippet = snippet ?? UIFont.monospacedSystemFont(ofSize: descr.pointSize, weight: .regular) + attrs[.font] = snippet + t = "/" + cmdStr + if !preview && commands { + attrs[.foregroundColor] = uiLinkColor + attrs[commandAttrKey] = t + handleTaps = true + } case let .mention(memberName): if let m = mentions?[memberName] { mention = mention ?? UIFont(descriptor: descr.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold]]), size: descr.pointSize) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 712a88114f..2f8d6f2acd 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -59,6 +59,7 @@ struct ChatView: View { @State private var ignoreLoadingRequests: Int64? = nil @State private var animatedScrollingInProgress: Bool = false @State private var showUserSupportChatSheet = false + @State private var showCommandsMenu = false @State private var scrollView: EndlessScrollView = EndlessScrollView(frame: .zero) @@ -109,6 +110,9 @@ struct ChatView: View { if let groupInfo = chat.chatInfo.groupInfo, !composeState.message.isEmpty { GroupMentionsView(im: im, groupInfo: groupInfo, composeState: $composeState, selectedRange: $selectedRange, keyboardVisible: $keyboardVisible) } + if !chat.chatInfo.menuCommands.isEmpty { + CommandsMenuView(chat: chat, composeState: $composeState, selectedRange: $selectedRange, showCommandsMenu: $showCommandsMenu) + } FloatingButtons(im: im, theme: theme, scrollView: scrollView, chat: chat, loadingMoreItems: $loadingMoreItems, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel, reloadItems: { mergedItems.boxedValue = MergedItems.create(im, revealedItems) scrollView.updateItems(mergedItems.boxedValue.items) @@ -135,6 +139,7 @@ struct ChatView: View { chat: chat, im: im, composeState: $composeState, + showCommandsMenu: $showCommandsMenu, keyboardVisible: $keyboardVisible, keyboardHiddenDate: $keyboardHiddenDate, selectedRange: $selectedRange, @@ -919,10 +924,12 @@ struct ChatView: View { case .inv: "Tap Connect to chat" case .con: - "Tap Connect to send request" + contact.isBot ? "Tap Connect to use bot" : "Tap Connect to send request" } } else if contact.nextAcceptContactRequest { "Accept contact request" + } else if case .bot = contact.profile.peerType { + "Bot" } else { "Your contact" } @@ -956,9 +963,7 @@ struct ChatView: View { switch (chat.chatInfo) { case let .direct(contact): if !contact.sndReady && contact.active && !contact.sendMsgToConnect && !contact.nextAcceptContactRequest { - contact.preparedContact?.uiConnLinkType == .con - ? "contact should accept…" - : contact.contactGroupMemberId != nil + (contact.preparedContact?.uiConnLinkType == .con && !contact.isBot) || contact.contactGroupMemberId != nil ? "contact should accept…" : "connecting…" } else { diff --git a/apps/ios/Shared/Views/Chat/CommandsMenuView.swift b/apps/ios/Shared/Views/Chat/CommandsMenuView.swift new file mode 100644 index 0000000000..525bf5725c --- /dev/null +++ b/apps/ios/Shared/Views/Chat/CommandsMenuView.swift @@ -0,0 +1,187 @@ +// +// CommandsMenuView.swift +// SimpleX (iOS) +// +// Created by EP on 03/08/2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +let COMMAND_ROW_SIZE: CGFloat = 48 +let MAX_VISIBLE_COMMAND_ROWS: CGFloat = 5.8 + +struct CommandsMenuView: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @ObservedObject var chat: Chat + @Binding var composeState: ComposeState + @Binding var selectedRange: NSRange + @Binding var showCommandsMenu: Bool + + @State private var currentCommands: [ChatBotCommand] = [] + @State private var menuTreeBackPath: [(label: String, commands: [ChatBotCommand])] = [] + @State private var keywordWidth: CGFloat = 0 + + var body: some View { + ZStack(alignment: .bottom) { + if !currentCommands.isEmpty { + Color.white.opacity(0.01) + .edgesIgnoringSafeArea(.all) + .onTapGesture { + showCommandsMenu = false + currentCommands = [] + menuTreeBackPath = [] + } + VStack(spacing: 0) { + Spacer() + let cmdsCount = currentCommands.count + (menuTreeBackPath.isEmpty ? 0 : 1) + let scroll = ScrollView { + VStack(spacing: 0) { + if let prev = menuTreeBackPath.last { + Divider() + menuLabelRow(prev) + } + ForEach(currentCommands, id: \.self, content: commandRow) + } + } + .frame(maxWidth: .infinity, maxHeight: COMMAND_ROW_SIZE * min(MAX_VISIBLE_COMMAND_ROWS, CGFloat(cmdsCount))) + .background(theme.colors.background) + + if #available(iOS 16.0, *) { + scroll.scrollDismissesKeyboard(.never) + } else { + scroll + } + } + .onPreferenceChange(DetermineWidth.Key.self) { keywordWidth = $0 } + } + } + .onChange(of: composeState.message) { message in + let msg = message.trimmingCharacters(in: .whitespaces) + if msg == "/" { + currentCommands = chat.chatInfo.menuCommands + } else if msg.first == "/" { + currentCommands = filterShownCommands(chat.chatInfo.menuCommands, msg.dropFirst()) + } else { + showCommandsMenu = false + currentCommands = [] + } + menuTreeBackPath = [] + } + .onChange(of: showCommandsMenu) { show in + currentCommands = show ? chat.chatInfo.menuCommands : [] + menuTreeBackPath = [] + } + } + + private func menuLabelRow(_ prev: (label: String, commands: [ChatBotCommand])) -> some View { + HStack { + Image(systemName: "chevron.left") + .foregroundColor(theme.colors.secondary) + Text(prev.label) + .fontWeight(.medium) + .frame(maxWidth: .infinity) + } + .padding(.horizontal) + .frame(maxWidth: .infinity, alignment: .leading) + .frame(height: COMMAND_ROW_SIZE, alignment: .center) + .contentShape(Rectangle()) + .onTapGesture { + if !menuTreeBackPath.isEmpty { + currentCommands = menuTreeBackPath.removeLast().commands + } + } + } + + @ViewBuilder + private func commandRow(_ command: ChatBotCommand) -> some View { + Divider() + switch command { + case let .command(keyword, label, params): + HStack { + Text(label) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + Text("/" + keyword) + .font(.subheadline) + .lineLimit(1) + .foregroundColor(theme.colors.secondary) + .frame(minWidth: keywordWidth, alignment: .trailing) + .overlay(DetermineWidth()) + } + .padding(.horizontal) + .frame(height: COMMAND_ROW_SIZE, alignment: .center) + .contentShape(Rectangle()) + .onTapGesture { + if let params { + composeState.message = "/\(keyword) \(params)" + selectedRange = NSRange(location: composeState.message.count, length: 0) + } else { + composeState.message = "" + sendCommandMsg(chat, "/\(keyword)") + } + showCommandsMenu = false + currentCommands = [] + menuTreeBackPath = [] + } + case let .menu(label, cmds): + HStack { + Text(label) + .fontWeight(.medium) + .lineLimit(1) + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(theme.colors.secondary) + } + .padding(.horizontal) + .frame(height: COMMAND_ROW_SIZE, alignment: .center) + .contentShape(Rectangle()) + .onTapGesture { + menuTreeBackPath.append((label: label, commands: currentCommands)) + currentCommands = cmds + } + } + } + + private func filterShownCommands(_ commands: [ChatBotCommand], _ msg: String.SubSequence) -> [ChatBotCommand] { + var cmds: [ChatBotCommand] = [] + for command in commands { + switch command { + case let .command(keyword, _, _): + if keyword.starts(with: msg) { + cmds.append(command) + } + case let .menu(_, innerCmds): + cmds.append(contentsOf: filterShownCommands(innerCmds, msg)) + } + } + return cmds + } +} + +func sendCommandMsg(_ chat: Chat, _ cmd: String) { + if chat.chatInfo.sndReady { + Task { + if let chatItems = await apiSendMessages( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + scope: chat.chatInfo.groupChatScope(), + composedMessages: [ComposedMessage(msgContent: .text(cmd))] + ) { + await MainActor.run { + for ci in chatItems { + ChatModel.shared.addChatItem(chat.chatInfo, ci) + } + } + } + } + } else { + showAlert( + NSLocalizedString("You can't send messages!", comment: "alert title"), + message: NSLocalizedString("To send commands you must be connected.", comment: "alert message"), + actions: { [okAlertAction] } + ) + } +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 876761a588..a6e6518e7b 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -323,6 +323,7 @@ struct ComposeView: View { @ObservedObject var chat: Chat @ObservedObject var im: ItemsModel @Binding var composeState: ComposeState + @Binding var showCommandsMenu: Bool @Binding var keyboardVisible: Bool @Binding var keyboardHiddenDate: Date @Binding var selectedRange: NSRange @@ -410,18 +411,7 @@ struct ComposeView: View { if chat.chatInfo.groupInfo?.nextConnectPrepared == true { if chat.chatInfo.groupInfo?.businessChat == nil { - Button(action: connectPreparedGroup) { - ZStack(alignment: .trailing) { - Label("Join group", systemImage: "person.2.fill") - .frame(maxWidth: .infinity) - if composeState.progressByTimeout { - ProgressView() - .padding() - } - } - } - .frame(height: 60) - .disabled(composeState.inProgress) + connectButtonView("Join group", icon: "person.2.fill", connect: connectPreparedGroup) } else { sendContactRequestView(disableSendButton, icon: "briefcase.fill", sendRequest: connectPreparedGroup) } @@ -429,27 +419,22 @@ struct ComposeView: View { contextSendMessageToConnect("Send direct message to connect") Divider() HStack (alignment: .center) { - attachmentButton().disabled(true) + attachmentAndCommandsButtons().disabled(true) sendMessageView(disableSendButton, sendToConnect: sendMemberContactInvitation) } .padding(.horizontal, 12) - } else if contact?.nextConnectPrepared == true, let linkType = contact?.preparedContact?.uiConnLinkType { + } else if let contact, + contact.nextConnectPrepared == true, + let linkType = contact.preparedContact?.uiConnLinkType { switch linkType { case .inv: - Button(action: sendConnectPreparedContact) { - ZStack(alignment: .trailing) { - Label("Connect", systemImage: "person.fill.badge.plus") - .frame(maxWidth: .infinity) - if composeState.progressByTimeout { - ProgressView() - .padding() - } - } - } - .frame(height: 60) - .disabled(composeState.inProgress) + connectButtonView("Connect", icon: "person.fill.badge.plus", connect: sendConnectPreparedContact) case .con: - sendContactRequestView(disableSendButton, icon: "person.fill.badge.plus", sendRequest: sendConnectPreparedContactRequest) + if contact.isBot { + connectButtonView("Connect", icon: "bolt.fill", connect: sendConnectPreparedContact) + } else { + sendContactRequestView(disableSendButton, icon: "person.fill.badge.plus", sendRequest: sendConnectPreparedContactRequest) + } } } else if contact?.nextAcceptContactRequest == true, let crId = contact?.contactRequestId { ContextContactRequestActionsView(contactRequestId: crId) @@ -457,7 +442,7 @@ struct ComposeView: View { ContextMemberContactActionsView(contact: ct, groupDirectInv: groupDirectInv) } else { HStack (alignment: .center) { - attachmentButton() + attachmentAndCommandsButtons() sendMessageView(disableSendButton) } .padding(.horizontal, 12) @@ -635,6 +620,21 @@ struct ComposeView: View { } } + private func connectButtonView(_ label: LocalizedStringKey, icon: String, connect: @escaping () -> Void) -> some View { + Button(action: connect) { + ZStack(alignment: .trailing) { + Label(label, systemImage: icon) + .frame(maxWidth: .infinity) + if composeState.progressByTimeout { + ProgressView() + .padding() + } + } + } + .frame(height: 60) + .disabled(composeState.inProgress) + } + private func sendContactRequestView(_ disableSendButton: Bool, icon: String, sendRequest: @escaping () -> Void) -> some View { HStack (alignment: .center) { sendMessageView( @@ -703,6 +703,35 @@ struct ComposeView: View { } } + @ViewBuilder private func attachmentAndCommandsButtons() -> some View { + let msg = composeState.message.trimmingCharacters(in: .whitespaces) + let showAttachment = chat.chatInfo.contact?.profile.peerType != .bot || chat.chatInfo.featureEnabled(.files) + let showCommands = chat.chatInfo.useCommands && (!showAttachment || msg.isEmpty || msg.starts(with: "/")) + if showCommands { + commandsButton() + } + if showAttachment { + attachmentButton() + .padding(.trailing, 3) + .if(showCommands) { v in v.padding(.leading, 3) } + } + } + + private func commandsButton() -> some View { + Button { + showCommandsMenu.toggle() + } label: { + Text(verbatim: "//") + .font(.title3) + .italic() + .contentShape(Rectangle()) + } + .disabled(!chat.chatInfo.sendMsgEnabled || chat.chatInfo.menuCommands.isEmpty) + .frame(width: 25, height: 25) + .tint(theme.colors.primary) + .padding(.bottom, 2) + } + @ViewBuilder private func attachmentButton() -> some View { let b = Button { showChooseSource = true @@ -714,12 +743,11 @@ struct ComposeView: View { .frame(width: 25, height: 25) .tint(theme.colors.primary) if im.secondaryIMFilter == nil, - case let .group(g, _) = chat.chatInfo, - !g.fullGroupPreferences.files.on(for: g.membership) { + !chat.chatInfo.featureEnabled(.files) { b.disabled(true).onTapGesture { AlertManager.shared.showAlertMsg( title: "Files and media prohibited!", - message: "Only group owners can enable files and media." + message: chat.chatInfo.groupInfo == nil ? nil : "Only group owners can enable files and media." ) } } else { @@ -750,7 +778,6 @@ struct ComposeView: View { } } - // TODO [short links] different messages for business private func sendConnectPreparedContactRequest() { hideKeyboard() let empty = composeState.whitespaceOnly @@ -1489,33 +1516,3 @@ struct ComposeView: View { cancelledLinks = [] } } - -struct ComposeView_Previews: PreviewProvider { - static var previews: some View { - let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) - let im = ItemsModel.shared - @State var composeState = ComposeState(message: "hello") - @State var selectedRange = NSRange() - - return Group { - ComposeView( - chat: chat, - im: im, - composeState: $composeState, - keyboardVisible: Binding.constant(true), - keyboardHiddenDate: Binding.constant(Date.now), - selectedRange: $selectedRange - ) - .environmentObject(ChatModel()) - ComposeView( - chat: chat, - im: im, - composeState: $composeState, - keyboardVisible: Binding.constant(true), - keyboardHiddenDate: Binding.constant(Date.now), - selectedRange: $selectedRange - ) - .environmentObject(ChatModel()) - } - } -} diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift index 07cc7bd217..440ed5227d 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift @@ -67,7 +67,7 @@ struct GroupMentionsView: View { } } .frame(maxHeight: MEMBER_ROW_SIZE * min(MAX_VISIBLE_MEMBER_ROWS, CGFloat(filtered.count))) - .background(Color(UIColor.systemBackground)) + .background(theme.colors.background) if #available(iOS 16.0, *) { scroll.scrollDismissesKeyboard(.never) diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 79f72e539a..c56d947a5a 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -351,12 +351,14 @@ struct ChatPreviewView: View { if contact.isContactCard { Text("Tap to Connect") .foregroundColor(theme.colors.primary) + } else if contact.isBot && contact.nextConnectPrepared { + Text("Open to use bot") } else if contact.sendMsgToConnect { Text("Open to connect") } else if contact.nextAcceptContactRequest { Text("Open to accept") } else if !contact.sndReady && contact.activeConn != nil && contact.active { - contact.preparedContact?.uiConnLinkType == .con + (contact.preparedContact?.uiConnLinkType == .con && !contact.isBot) || contact.contactGroupMemberId != nil ? Text("contact should accept…") : Text("connecting…") } else { diff --git a/apps/ios/Shared/Views/Helpers/ProfileImage.swift b/apps/ios/Shared/Views/Helpers/ProfileImage.swift index 4cc244cb24..9c2916880c 100644 --- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift +++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift @@ -27,6 +27,7 @@ struct ProfileImage: View { Image(systemName: iconName) .resizable() .foregroundColor(c) + .scaledToFit() .frame(width: size, height: size) .background( Circle() diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 432422d77b..3de1fdb972 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -1039,7 +1039,11 @@ private func showPrepareContactAlert( profileImage: ProfileImage( imageStr: contactShortLinkData.profile.image, - iconName: contactShortLinkData.business ? "briefcase.circle.fill" : "person.crop.circle.fill", + iconName: contactShortLinkData.business + ? "briefcase.circle.fill" + : contactShortLinkData.profile.peerType == .bot + ? "cube.fill" + : "person.crop.circle.fill", size: alertProfileImageSize ), theme: theme, @@ -1112,7 +1116,7 @@ private func showOpenKnownContactAlert( profileImage: ProfileImage( imageStr: contact.profile.image, - iconName: "person.crop.circle.fill", + iconName: contact.chatIconName, size: alertProfileImageSize ), theme: theme, @@ -1138,7 +1142,7 @@ private func showOpenKnownGroupAlert( profileImage: ProfileImage( imageStr: groupInfo.groupProfile.image, - iconName: groupInfo.businessChat == nil ? "person.2.circle.fill" : "briefcase.circle.fill", + iconName: groupInfo.chatIconName, size: alertProfileImageSize ), theme: theme, diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index c4f75e1989..bfa410ca8e 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -855,6 +855,10 @@ swipe action Позволи понижаване No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава. (24 часа) @@ -939,6 +943,10 @@ swipe action Позволи на вашите контакти да изпращат изчезващи съобщения. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Позволи на вашите контакти да изпращат гласови съобщения. @@ -1287,6 +1295,10 @@ swipe action Размазване на медия No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. И вие, и вашият контакт можете да добавяте реакции към съобщението. @@ -1307,6 +1319,10 @@ swipe action И вие, и вашият контакт можете да изпращате изчезващи съобщения. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. И вие, и вашият контакт можете да изпращате гласови съобщения. @@ -3607,6 +3623,10 @@ snd error text Файлове и медия chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Файловете и медията са забранени в тази група. @@ -5378,6 +5398,10 @@ Requires compatible VPN. Само вие можете да изпращате изчезващи съобщения. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Само вие можете да изпращате гласови съобщения. @@ -5403,6 +5427,10 @@ Requires compatible VPN. Само вашият контакт може да изпраща изчезващи съобщения. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Само вашият контакт може да изпраща гласови съобщения. @@ -5470,6 +5498,10 @@ Requires compatible VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Приложението се отваря… @@ -7431,6 +7463,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -7815,6 +7851,10 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. За поддръжка на незабавни push известия, базата данни за чат трябва да бъде мигрирана. @@ -8658,7 +8698,7 @@ Repeat join request? You can't send messages! Не може да изпращате съобщения! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 0ae8e0a70a..87524b09ab 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -819,6 +819,10 @@ swipe action Allow downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí. (24 hodin) @@ -901,6 +905,10 @@ swipe action Umožněte svým kontaktům odesílat mizící zprávy. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Povolte svým kontaktům odesílání hlasových zpráv. @@ -1219,6 +1227,10 @@ swipe action Blur media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Vy i váš kontakt můžete přidávat reakce na zprávy. @@ -1239,6 +1251,10 @@ swipe action Vy i váš kontakt můžete posílat mizící zprávy. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Hlasové zprávy můžete posílat vy i váš kontakt. @@ -3469,6 +3485,10 @@ snd error text Soubory a média chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Soubory a média jsou zakázány v této skupině. @@ -5185,6 +5205,10 @@ Vyžaduje povolení sítě VPN. Mizící zprávy můžete odesílat pouze vy. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Hlasové zprávy můžete posílat pouze vy. @@ -5210,6 +5234,10 @@ Vyžaduje povolení sítě VPN. Zmizelé zprávy může odesílat pouze váš kontakt. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Hlasové zprávy může odesílat pouze váš kontakt. @@ -5275,6 +5303,10 @@ Vyžaduje povolení sítě VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… No comment provided by engineer. @@ -7190,6 +7222,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -7564,6 +7600,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Pro podporu doručování okamžitých upozornění musí být přenesena chat databáze. @@ -8365,7 +8405,7 @@ Repeat join request? You can't send messages! Nemůžete posílat zprávy! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index cda7e1ead0..3a9c362405 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -867,6 +867,10 @@ swipe action Herabstufung erlauben No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt. (24 Stunden) @@ -952,6 +956,10 @@ swipe action Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Erlauben Sie Ihren Kontakten Sprachnachrichten zu senden. @@ -1312,6 +1320,10 @@ swipe action Medien verpixeln No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Sowohl Sie, als auch Ihr Kontakt können Reaktionen auf Nachrichten geben. @@ -1332,6 +1344,10 @@ swipe action Ihr Kontakt und Sie können beide verschwindende Nachrichten senden. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten senden. @@ -3794,6 +3810,10 @@ snd error text Dateien und Medien chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. In dieser Gruppe sind Dateien und Medien nicht erlaubt. @@ -5684,6 +5704,10 @@ Dies erfordert die Aktivierung eines VPNs. Nur Sie können verschwindende Nachrichten senden. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Nur Sie können Sprachnachrichten versenden. @@ -5709,6 +5733,10 @@ Dies erfordert die Aktivierung eines VPNs. Nur Ihr Kontakt kann verschwindende Nachrichten senden. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Nur Ihr Kontakt kann Sprachnachrichten versenden. @@ -5784,6 +5812,10 @@ Dies erfordert die Aktivierung eines VPNs. Zum Beitreten öffnen No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… App wird geöffnet… @@ -7914,6 +7946,10 @@ report reason Verbinden tippen, um die Anfrage zu senden No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen. @@ -8327,6 +8363,10 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Für das Senden No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden. @@ -9224,7 +9264,7 @@ Verbindungsanfrage wiederholen? You can't send messages! Sie können keine Nachrichten versenden! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 947f0a2d6c..05b9b9bc9d 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -867,6 +867,11 @@ swipe action Allow downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Allow irreversible message deletion only if your contact allows it to you. (24 hours) @@ -952,6 +957,11 @@ swipe action Allow your contacts to send disappearing messages. No comment provided by engineer. + + Allow your contacts to send files and media. + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Allow your contacts to send voice messages. @@ -1312,6 +1322,11 @@ swipe action Blur media No comment provided by engineer. + + Bot + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Both you and your contact can add message reactions. @@ -1332,6 +1347,11 @@ swipe action Both you and your contact can send disappearing messages. No comment provided by engineer. + + Both you and your contact can send files and media. + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Both you and your contact can send voice messages. @@ -3796,6 +3816,11 @@ snd error text Files and media chat feature + + Files and media are prohibited in this chat. + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Files and media are prohibited. @@ -5687,6 +5712,11 @@ Requires compatible VPN. Only you can send disappearing messages. No comment provided by engineer. + + Only you can send files and media. + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Only you can send voice messages. @@ -5712,6 +5742,11 @@ Requires compatible VPN. Only your contact can send disappearing messages. No comment provided by engineer. + + Only your contact can send files and media. + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Only your contact can send voice messages. @@ -5787,6 +5822,11 @@ Requires compatible VPN. Open to join No comment provided by engineer. + + Open to use bot + Open to use bot + No comment provided by engineer. + Opening app… Opening app… @@ -7917,6 +7957,11 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Tap Create SimpleX address in the menu to create it later. @@ -8331,6 +8376,11 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. To support instant push notifications the chat database has to be migrated. @@ -9228,7 +9278,7 @@ Repeat join request? You can't send messages! You can't send messages! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index b58f716f8e..1454ad639d 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -867,6 +867,10 @@ swipe action Permitir versión anterior No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también la permite para tí. (24 horas) @@ -952,6 +956,10 @@ swipe action Permites a tus contactos enviar mensajes temporales. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Permites a tus contactos enviar mensajes de voz. @@ -1312,6 +1320,10 @@ swipe action Difuminar multimedia No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Tanto tú como tu contacto podéis añadir reacciones a los mensajes. @@ -1332,6 +1344,10 @@ swipe action Tanto tú como tu contacto podéis enviar mensajes temporales. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Tanto tú como tu contacto podéis enviar mensajes de voz. @@ -3794,6 +3810,10 @@ snd error text Archivos y multimedia chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Los archivos y multimedia no están permitidos en este grupo. @@ -5684,6 +5704,10 @@ Requiere activación de la VPN. Sólo tú puedes enviar mensajes temporales. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Sólo tú puedes enviar mensajes de voz. @@ -5709,6 +5733,10 @@ Requiere activación de la VPN. Sólo tu contacto puede enviar mensajes temporales. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Sólo tu contacto puede enviar mensajes de voz. @@ -5784,6 +5812,10 @@ Requiere activación de la VPN. Abrir para unirte No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Iniciando aplicación… @@ -7914,6 +7946,10 @@ report reason Pulsa Conectar para enviar solicitud No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Pulsa Crear dirección SimpleX en el menú para crearla más tarde. @@ -8327,6 +8363,10 @@ Se te pedirá que completes la autenticación antes de activar esta función.Para enviar No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Para permitir las notificaciones automáticas instantáneas, la base de datos se debe migrar. @@ -9224,7 +9264,7 @@ Repeat join request? You can't send messages! ¡No puedes enviar mensajes! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index b5ba7bb864..4b42238a22 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -792,6 +792,10 @@ swipe action Allow downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle. (24 tuntia) @@ -874,6 +878,10 @@ swipe action Salli kontaktiesi lähettää katoavia viestejä. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Salli kontaktiesi lähettää ääniviestejä. @@ -1191,6 +1199,10 @@ swipe action Blur media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Sekä sinä että kontaktisi voivat käyttää viestireaktioita. @@ -1211,6 +1223,10 @@ swipe action Sekä sinä että kontaktisi voitte lähettää katoavia viestejä. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Sekä sinä että kontaktisi voitte lähettää ääniviestejä. @@ -3437,6 +3453,10 @@ snd error text Tiedostot ja media chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Tiedostot ja media ovat tässä ryhmässä kiellettyjä. @@ -5152,6 +5172,10 @@ Edellyttää VPN:n sallimista. Vain sinä voit lähettää katoavia viestejä. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Vain sinä voit lähettää ääniviestejä. @@ -5177,6 +5201,10 @@ Edellyttää VPN:n sallimista. Vain kontaktisi voi lähettää katoavia viestejä. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Vain kontaktisi voi lähettää ääniviestejä. @@ -5241,6 +5269,10 @@ Edellyttää VPN:n sallimista. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… No comment provided by engineer. @@ -7154,6 +7186,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -7528,6 +7564,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Keskustelujen-tietokanta on siirrettävä välittömien push-ilmoitusten tukemiseksi. @@ -8328,7 +8368,7 @@ Repeat join request? You can't send messages! Et voi lähettää viestejä! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 8072b5784b..36c1fdfc91 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -861,6 +861,10 @@ swipe action Autoriser la rétrogradation No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Autoriser la suppression irréversible des messages uniquement si votre contact vous l'autorise. (24 heures) @@ -946,6 +950,10 @@ swipe action Autorise votre contact à envoyer des messages éphémères. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Autorise vos contacts à envoyer des messages vocaux. @@ -1303,6 +1311,10 @@ swipe action Flouter les médias No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Vous et votre contact pouvez ajouter des réactions aux messages. @@ -1323,6 +1335,10 @@ swipe action Vous et votre contact êtes tous deux en mesure d'envoyer des messages éphémères. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Vous et votre contact êtes tous deux en mesure d'envoyer des messages vocaux. @@ -3767,6 +3783,10 @@ snd error text Fichiers et médias chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Les fichiers et les médias sont interdits dans ce groupe. @@ -5611,6 +5631,10 @@ Nécessite l'activation d'un VPN. Seulement vous pouvez envoyer des messages éphémères. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Vous seul pouvez envoyer des messages vocaux. @@ -5636,6 +5660,10 @@ Nécessite l'activation d'un VPN. Seulement votre contact peut envoyer des messages éphémères. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Seul votre contact peut envoyer des messages vocaux. @@ -5705,6 +5733,10 @@ Nécessite l'activation d'un VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Ouverture de l'app… @@ -7779,6 +7811,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Appuyez sur Créer une adresse SimpleX dans le menu pour la créer ultérieurement. @@ -8184,6 +8220,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Pour envoyer No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Pour prendre en charge les notifications push instantanées, la base de données du chat doit être migrée. @@ -9065,7 +9105,7 @@ Répéter la demande d'adhésion ? You can't send messages! Vous ne pouvez pas envoyer de messages ! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 1339f0ce74..156d12ea9f 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -867,6 +867,10 @@ swipe action Visszafejlesztés engedélyezése No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. (24 óra) @@ -952,6 +956,10 @@ swipe action Az eltűnő üzenetek küldésének engedélyezése a partnerei számára. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. A hangüzenetek küldése engedélyezve van a partnerei számára. @@ -1312,6 +1320,10 @@ swipe action Médiatartalom elhomályosítása No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Mindkét fél hozzáadhat az üzenetekhez reakciókat. @@ -1332,6 +1344,10 @@ swipe action Mindkét fél küldhet eltűnő üzeneteket. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Mindkét fél küldhet hangüzeneteket. @@ -3794,6 +3810,10 @@ snd error text Fájlok és médiatartalmak chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. A fájlok- és a médiatartalmak küldése le van tiltva. @@ -5684,6 +5704,10 @@ VPN engedélyezése szükséges. Csak Ön tud eltűnő üzeneteket küldeni. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Csak Ön tud hangüzeneteket küldeni. @@ -5709,6 +5733,10 @@ VPN engedélyezése szükséges. Csak a partnere tud eltűnő üzeneteket küldeni. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Csak a partnere tud hangüzeneteket küldeni. @@ -5784,6 +5812,10 @@ VPN engedélyezése szükséges. Megnyitás a csatlakozáshoz No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Az alkalmazás megnyitása… @@ -7914,6 +7946,10 @@ report reason Koppintson a „Kapcsolódás” gombra a kérés elküldéséhez No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz. @@ -8327,6 +8363,10 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll A küldéshez No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Az azonnali push-értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges. @@ -9224,7 +9264,7 @@ Megismétli a csatlakozási kérést? You can't send messages! Nem lehet üzeneteket küldeni! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 4b51e9dae3..5d3e962d25 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -867,6 +867,10 @@ swipe action Consenti downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Consenti l'eliminazione irreversibile dei messaggi solo se il contatto la consente a te. (24 ore) @@ -952,6 +956,10 @@ swipe action Permetti ai tuoi contatti di inviare messaggi a tempo. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Permetti ai tuoi contatti di inviare messaggi vocali. @@ -1312,6 +1320,10 @@ swipe action Sfocatura dei file multimediali No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Sia tu che il tuo contatto potete aggiungere reazioni ai messaggi. @@ -1332,6 +1344,10 @@ swipe action Sia tu che il tuo contatto potete inviare messaggi a tempo. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Sia tu che il tuo contatto potete inviare messaggi vocali. @@ -3794,6 +3810,10 @@ snd error text File e multimediali chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. File e contenuti multimediali sono vietati in questo gruppo. @@ -5684,6 +5704,10 @@ Richiede l'attivazione della VPN. Solo tu puoi inviare messaggi a tempo. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Solo tu puoi inviare messaggi vocali. @@ -5709,6 +5733,10 @@ Richiede l'attivazione della VPN. Solo il tuo contatto può inviare messaggi a tempo. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Solo il tuo contatto può inviare messaggi vocali. @@ -5784,6 +5812,10 @@ Richiede l'attivazione della VPN. Apri per entrare No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Apertura dell'app… @@ -7914,6 +7946,10 @@ report reason Tocca Connetti per inviare la richiesta No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi. @@ -8327,6 +8363,10 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Per inviare No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Per supportare le notifiche push istantanee, il database della chat deve essere migrato. @@ -9224,7 +9264,7 @@ Ripetere la richiesta di ingresso? You can't send messages! Non puoi inviare messaggi! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 743c5392e6..b76edfc106 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -831,6 +831,10 @@ swipe action Allow downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) 送信相手も永久メッセージ削除を許可する時のみに許可する。(24時間) @@ -915,6 +919,10 @@ swipe action 送信相手が消えるメッセージを送るのを許可する。 No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. 送信相手からの音声メッセージを許可する。 @@ -1240,6 +1248,10 @@ swipe action Blur media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. 自分も相手もメッセージへのリアクションを追加できます。 @@ -1260,6 +1272,10 @@ swipe action あなたと連絡相手が消えるメッセージを送信できます。 No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. あなたと連絡相手が音声メッセージを送信できます。 @@ -3510,6 +3526,10 @@ snd error text ファイルとメディア chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. このグループでは、ファイルとメディアは禁止されています。 @@ -5228,6 +5248,10 @@ VPN を有効にする必要があります。 消えるメッセージを送れるのはあなただけです。 No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. 音声メッセージを送れるのはあなただけです。 @@ -5253,6 +5277,10 @@ VPN を有効にする必要があります。 消えるメッセージを送れるのはあなたの連絡相手だけです。 No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. 音声メッセージを送れるのはあなたの連絡相手だけです。 @@ -5318,6 +5346,10 @@ VPN を有効にする必要があります。 Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… No comment provided by engineer. @@ -7225,6 +7257,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -7598,6 +7634,10 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. インスタント プッシュ通知をサポートするには、チャット データベースを移行する必要があります。 @@ -8399,7 +8439,7 @@ Repeat join request? You can't send messages! メッセージを送信できませんでした! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 6d65bd070f..495fd3ee1a 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -865,6 +865,10 @@ swipe action Downgraden toestaan No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Sta het definitief verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) @@ -950,6 +954,10 @@ swipe action Sta toe dat uw contacten verdwijnende berichten verzenden. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Sta toe dat uw contacten spraak berichten verzenden. @@ -1308,6 +1316,10 @@ swipe action Vervaag media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Zowel u als uw contact kunnen bericht reacties toevoegen. @@ -1328,6 +1340,10 @@ swipe action Zowel jij als je contact kunnen verdwijnende berichten sturen. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Zowel jij als je contact kunnen spraak berichten verzenden. @@ -3778,6 +3794,10 @@ snd error text Bestanden en media chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Bestanden en media zijn niet toegestaan. @@ -5660,6 +5680,10 @@ Vereist het inschakelen van VPN. Alleen jij kunt verdwijnende berichten verzenden. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Alleen jij kunt spraak berichten verzenden. @@ -5685,6 +5709,10 @@ Vereist het inschakelen van VPN. Alleen uw contact kan verdwijnende berichten verzenden. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Alleen uw contact kan spraak berichten verzenden. @@ -5755,6 +5783,10 @@ Vereist het inschakelen van VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… App openen… @@ -7866,6 +7898,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Tik op SimpleX-adres maken in het menu om het later te maken. @@ -8275,6 +8311,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Om te verzenden No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Om directe push meldingen te ondersteunen, moet de chat database worden gemigreerd. @@ -9163,7 +9203,7 @@ Deelnameverzoek herhalen? You can't send messages! Je kunt geen berichten versturen! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 5dd679dfff..224bc41e42 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -861,6 +861,10 @@ swipe action Zezwól na obniżenie wersji No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli. (24 godziny) @@ -946,6 +950,10 @@ swipe action Zezwól swoim kontaktom na wysyłanie znikających wiadomości. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Zezwól swoim kontaktom na wysyłanie wiadomości głosowych. @@ -1302,6 +1310,10 @@ swipe action Rozmycie mediów No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Zarówno Ty, jak i Twój kontakt możecie dodawać reakcje wiadomości. @@ -1322,6 +1334,10 @@ swipe action Zarówno Ty, jak i Twój kontakt możecie wysyłać znikające wiadomości. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe. @@ -3708,6 +3724,10 @@ snd error text Pliki i media chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Pliki i media są zabronione w tej grupie. @@ -5525,6 +5545,10 @@ Wymaga włączenia VPN. Tylko Ty możesz wysyłać znikające wiadomości. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Tylko Ty możesz wysyłać wiadomości głosowe. @@ -5550,6 +5574,10 @@ Wymaga włączenia VPN. Tylko Twój kontakt może wysyłać znikające wiadomości. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Tylko Twój kontakt może wysyłać wiadomości głosowe. @@ -5617,6 +5645,10 @@ Wymaga włączenia VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Otwieranie aplikacji… @@ -7669,6 +7701,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -8064,6 +8100,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Aby obsługiwać natychmiastowe powiadomienia push, należy zmigrować bazę danych czatu. @@ -8933,7 +8973,7 @@ Powtórzyć prośbę dołączenia? You can't send messages! Nie możesz wysyłać wiadomości! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 156c341f8a..00f91be200 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -867,6 +867,10 @@ swipe action Разрешить прямую доставку No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам. (24 часа) @@ -952,6 +956,10 @@ swipe action Разрешить Вашим контактам отправлять исчезающие сообщения. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Разрешить Вашим контактам отправлять голосовые сообщения. @@ -1312,6 +1320,10 @@ swipe action Размытие изображений No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. И Вы, и Ваш контакт можете добавлять реакции на сообщения. @@ -1332,6 +1344,10 @@ swipe action Вы и Ваш контакт можете отправлять исчезающие сообщения. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Вы и Ваш контакт можете отправлять голосовые сообщения. @@ -3796,6 +3812,10 @@ snd error text Файлы и медиа chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Файлы и медиа запрещены в этой группе. @@ -5686,6 +5706,10 @@ Requires compatible VPN. Только Вы можете отправлять исчезающие сообщения. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Только Вы можете отправлять голосовые сообщения. @@ -5711,6 +5735,10 @@ Requires compatible VPN. Только Ваш контакт может отправлять исчезающие сообщения. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Только Ваш контакт может отправлять голосовые сообщения. @@ -5786,6 +5814,10 @@ Requires compatible VPN. Откройте чтобы вступить No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Приложение отрывается… @@ -7916,6 +7948,10 @@ report reason Нажмите Соединиться, чтобы отправить запрос No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Нажмите Создать адрес SimpleX в меню, чтобы создать его позже. @@ -8330,6 +8366,10 @@ You will be prompted to complete authentication before this feature is enabled.< Для оправки No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены. @@ -9227,7 +9267,7 @@ Repeat join request? You can't send messages! Вы не можете отправлять сообщения! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index b7fbf073bd..2c514afc72 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -784,6 +784,10 @@ swipe action Allow downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น @@ -866,6 +870,10 @@ swipe action อนุญาตให้ผู้ติดต่อของคุณส่งข้อความที่จะหายไปหลังปิดแชท (disappearing messages) No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. อนุญาตให้ผู้ติดต่อของคุณส่งข้อความเสียง @@ -1183,6 +1191,10 @@ swipe action Blur media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้ @@ -1203,6 +1215,10 @@ swipe action ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความที่หายไปได้ No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความเสียงได้ @@ -3422,6 +3438,10 @@ snd error text ไฟล์และสื่อ chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้ @@ -5131,6 +5151,10 @@ Requires compatible VPN. มีเพียงคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้ No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. มีเพียงคุณเท่านั้นที่สามารถส่งข้อความเสียงได้ @@ -5156,6 +5180,10 @@ Requires compatible VPN. เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้ No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. ผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความเสียงได้ @@ -5220,6 +5248,10 @@ Requires compatible VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… No comment provided by engineer. @@ -7127,6 +7159,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -7500,6 +7536,10 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. เพื่อรองรับการแจ้งเตือนแบบทันที ฐานข้อมูลการแชทจะต้องได้รับการโยกย้าย @@ -8298,7 +8338,7 @@ Repeat join request? You can't send messages! คุณไม่สามารถส่งข้อความได้! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index cd07746337..5f26c285f5 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -867,6 +867,10 @@ swipe action Sürüm düşürmeye izin ver No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Konuştuğun kişi, kalıcı olarak silinebilen mesajlara izin veriyorsa sen de ver. (24 saat içinde) @@ -952,6 +956,10 @@ swipe action Kişilerinizin kaybolan mesajlar göndermesine izin verin. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Kişilerinizin sesli mesajlar göndermesine izin verin. @@ -1312,6 +1320,10 @@ swipe action Medyayı bulanıklaştır No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Sen ve konuştuğun kişi mesaj tepkileri ekleyebilir. @@ -1332,6 +1344,10 @@ swipe action Sen ve konuştuğun kişi kaybolan mesajlar gönderebilir. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Sen ve konuştuğun kişi sesli mesaj gönderebilir. @@ -3792,6 +3808,10 @@ snd error text Dosyalar ve medya chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Dosyalar ve medya bu grupta yasaklandı. @@ -5681,6 +5701,10 @@ VPN'nin etkinleştirilmesi gerekir. Sadece sen kaybolan mesajlar gönderebilirsin. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Sadece sen sesli mesajlar gönderebilirsin. @@ -5706,6 +5730,10 @@ VPN'nin etkinleştirilmesi gerekir. Sadece karşıdaki kişi kaybolan mesajlar gönderebilir. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Sadece karşıdaki kişi sesli mesajlar gönderebilir. @@ -5781,6 +5809,10 @@ VPN'nin etkinleştirilmesi gerekir. Katılmak için aç No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Uygulama açılıyor… @@ -7906,6 +7938,10 @@ report reason Bağlan'a dokunarak isteği gönderin No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Daha sonra oluşturmak için menüden BasitX adresi oluştur'a dokunun. @@ -8316,6 +8352,10 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Göndermek için No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Anlık anlık bildirimleri desteklemek için sohbet veritabanının taşınması gerekir. @@ -9205,7 +9245,7 @@ Katılma isteği tekrarlansın mı? You can't send messages! Mesajlar gönderemezsiniz! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 9b6ff96324..d87f1cc613 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -867,6 +867,10 @@ swipe action Дозволити пониження версії No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити. (24 години) @@ -952,6 +956,10 @@ swipe action Дозвольте своїм контактам надсилати зникаючі повідомлення. No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Дозвольте своїм контактам надсилати голосові повідомлення. @@ -1312,6 +1320,10 @@ swipe action Розмиття медіа No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Реакції на повідомлення можете додавати як ви, так і ваш контакт. @@ -1332,6 +1344,10 @@ swipe action Ви і ваш контакт можете надсилати зникаючі повідомлення. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Надсилати голосові повідомлення можете як ви, так і ваш контакт. @@ -3794,6 +3810,10 @@ snd error text Файли і медіа chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Файли та медіа в цій групі заборонені. @@ -5684,6 +5704,10 @@ Requires compatible VPN. Тільки ви можете надсилати зникаючі повідомлення. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Тільки ви можете надсилати голосові повідомлення. @@ -5709,6 +5733,10 @@ Requires compatible VPN. Тільки ваш контакт може надсилати зникаючі повідомлення. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Тільки ваш контакт може надсилати голосові повідомлення. @@ -5784,6 +5812,10 @@ Requires compatible VPN. Відкрито для приєднання No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Відкриваємо програму… @@ -7914,6 +7946,10 @@ report reason Натисніть Підключитися, щоб відправити запит No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Натисніть «Створити адресу SimpleX» у меню, щоб створити її пізніше. @@ -8327,6 +8363,10 @@ You will be prompted to complete authentication before this feature is enabled.< Щоб відправити No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату. @@ -9224,7 +9264,7 @@ Repeat join request? You can't send messages! Ви не можете надсилати повідомлення! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index fce4207e84..f8767b14e4 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -862,6 +862,10 @@ swipe action 允许降级 No comment provided by engineer. + + Allow files and media only if your contact allows them. + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) 仅有您的联系人许可后才允许不可撤回消息移除 @@ -947,6 +951,10 @@ swipe action 允许您的联系人发送限时消息。 No comment provided by engineer. + + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. 允许您的联系人发送语音消息。 @@ -1305,6 +1313,10 @@ swipe action 模糊媒体 No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. 您和您的联系人都可以添加消息回应。 @@ -1325,6 +1337,10 @@ swipe action 您和您的联系人都可以发送限时消息。 No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. 您和您的联系人都可以发送语音消息。 @@ -3767,6 +3783,10 @@ snd error text 文件和媒体 chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. 此群组中禁止文件和媒体。 @@ -5645,6 +5665,10 @@ Requires compatible VPN. 只有您可以发送限时消息。 No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. 只有您可以发送语音消息。 @@ -5670,6 +5694,10 @@ Requires compatible VPN. 只有您的联系人才可以发送限时消息。 No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. 只有您的联系人可以发送语音消息。 @@ -5739,6 +5767,10 @@ Requires compatible VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… 正在打开应用程序… @@ -7794,6 +7826,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -8186,6 +8222,10 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. 为了支持即时推送通知,聊天数据库必须被迁移。 @@ -9053,7 +9093,7 @@ Repeat join request? You can't send messages! 您无法发送消息! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index a46384d99b..d79dde0582 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -245,6 +245,7 @@ D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; }; D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; }; E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; }; + E559A0A12E3F77EE00B26F74 /* CommandsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E559A0A02E3F77EE00B26F74 /* CommandsMenuView.swift */; }; E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; }; E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; }; @@ -607,6 +608,7 @@ D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; }; D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = ""; }; + E559A0A02E3F77EE00B26F74 /* CommandsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandsMenuView.swift; sourceTree = ""; }; E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; @@ -771,6 +773,7 @@ 5C2E260E27A30FDC00F70299 /* ChatView.swift */, 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */, 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */, + E559A0A02E3F77EE00B26F74 /* CommandsMenuView.swift */, 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */, 5CE4407127ADB1D0007B033A /* Emoji.swift */, 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */, @@ -1512,6 +1515,7 @@ 8CBC14862D357CDB00BBD901 /* StorageView.swift in Sources */, 5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */, 5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */, + E559A0A12E3F77EE00B26F74 /* CommandsMenuView.swift in Sources */, 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */, 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */, 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */, diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index db370efdc1..f868b787ee 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -115,7 +115,8 @@ public struct Profile: Codable, NamedChat, Hashable { shortDescr: String? = nil, image: String? = nil, contactLink: String? = nil, - preferences: Preferences? = nil + preferences: Preferences? = nil, + peerType: ChatPeerType? = nil ) { self.displayName = displayName self.fullName = fullName @@ -131,6 +132,7 @@ public struct Profile: Codable, NamedChat, Hashable { public var image: String? public var contactLink: String? public var preferences: Preferences? + public var peerType: ChatPeerType? public var localAlias: String { get { "" } } var profileViewName: String { @@ -152,6 +154,7 @@ public struct LocalProfile: Codable, NamedChat, Hashable { image: String? = nil, contactLink: String? = nil, preferences: Preferences? = nil, + peerType: ChatPeerType? = nil, localAlias: String ) { self.profileId = profileId @@ -161,6 +164,7 @@ public struct LocalProfile: Codable, NamedChat, Hashable { self.image = image self.contactLink = contactLink self.preferences = preferences + self.peerType = peerType self.localAlias = localAlias } @@ -171,6 +175,7 @@ public struct LocalProfile: Codable, NamedChat, Hashable { public var image: String? public var contactLink: String? public var preferences: Preferences? + public var peerType: ChatPeerType? public var localAlias: String var profileViewName: String { @@ -188,6 +193,11 @@ public struct LocalProfile: Codable, NamedChat, Hashable { ) } +public enum ChatPeerType: String, Codable { + case human + case bot +} + public func toLocalProfile (_ profileId: Int64, _ profile: Profile, _ localAlias: String) -> LocalProfile { LocalProfile( profileId: profileId, @@ -197,6 +207,7 @@ public func toLocalProfile (_ profileId: Int64, _ profile: Profile, _ localAlias image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences, + peerType: profile.peerType, localAlias: localAlias ) } @@ -208,7 +219,8 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile { shortDescr: profile.shortDescr, image: profile.image, contactLink: profile.contactLink, - preferences: profile.preferences + preferences: profile.preferences, + peerType: profile.peerType ) } @@ -249,20 +261,26 @@ public struct FullPreferences: Decodable, Equatable, Hashable { public var fullDelete: SimplePreference public var reactions: SimplePreference public var voice: SimplePreference + public var files: SimplePreference public var calls: SimplePreference + public var commands: [ChatBotCommand] public init( timedMessages: TimedMessagesPreference, fullDelete: SimplePreference, reactions: SimplePreference, voice: SimplePreference, - calls: SimplePreference + files: SimplePreference, + calls: SimplePreference, + commands: [ChatBotCommand] ) { self.timedMessages = timedMessages self.fullDelete = fullDelete self.reactions = reactions self.voice = voice + self.files = files self.calls = calls + self.commands = commands } public static let sampleData = FullPreferences( @@ -270,7 +288,9 @@ public struct FullPreferences: Decodable, Equatable, Hashable { fullDelete: SimplePreference(allow: .no), reactions: SimplePreference(allow: .yes), voice: SimplePreference(allow: .yes), - calls: SimplePreference(allow: .yes) + files: SimplePreference(allow: .always), + calls: SimplePreference(allow: .yes), + commands: [] ) } @@ -279,20 +299,26 @@ public struct Preferences: Codable, Hashable { public var fullDelete: SimplePreference? public var reactions: SimplePreference? public var voice: SimplePreference? + public var files: SimplePreference? public var calls: SimplePreference? + public var commands: [ChatBotCommand]? public init( timedMessages: TimedMessagesPreference?, fullDelete: SimplePreference?, reactions: SimplePreference?, voice: SimplePreference?, - calls: SimplePreference? + files: SimplePreference?, + calls: SimplePreference?, + commands: [ChatBotCommand]? ) { self.timedMessages = timedMessages self.fullDelete = fullDelete self.reactions = reactions self.voice = voice + self.files = files self.calls = calls + self.commands = commands } func copy( @@ -300,14 +326,18 @@ public struct Preferences: Codable, Hashable { fullDelete: SimplePreference? = nil, reactions: SimplePreference? = nil, voice: SimplePreference? = nil, - calls: SimplePreference? = nil + files: SimplePreference? = nil, + calls: SimplePreference? = nil, + commands: [ChatBotCommand]? = nil ) -> Preferences { Preferences( timedMessages: timedMessages ?? self.timedMessages, fullDelete: fullDelete ?? self.fullDelete, reactions: reactions ?? self.reactions, voice: voice ?? self.voice, - calls: calls ?? self.calls + files: files ?? self.files, + calls: calls ?? self.calls, + commands: commands ?? self.commands ) } @@ -317,6 +347,7 @@ public struct Preferences: Codable, Hashable { case .fullDelete: return copy(fullDelete: SimplePreference(allow: allowed)) case .reactions: return copy(reactions: SimplePreference(allow: allowed)) case .voice: return copy(voice: SimplePreference(allow: allowed)) + case .files: return copy(voice: SimplePreference(allow: allowed)) case .calls: return copy(calls: SimplePreference(allow: allowed)) } } @@ -326,17 +357,72 @@ public struct Preferences: Codable, Hashable { fullDelete: SimplePreference(allow: .no), reactions: SimplePreference(allow: .yes), voice: SimplePreference(allow: .yes), - calls: SimplePreference(allow: .yes) + files: SimplePreference(allow: .always), + calls: SimplePreference(allow: .yes), + commands: nil ) } +public indirect enum ChatBotCommand: Hashable { + case command(keyword: String, label: String, params: String?) + case menu(label: String, commands: [ChatBotCommand]) + + enum CodingKeys: String, CodingKey { + case type + case keyword + case label + case params + case hidden + case commands + } +} + +extension ChatBotCommand: Decodable { + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + let type = try c.decode(String.self, forKey: CodingKeys.type) + switch type { + case "command": + let keyword = try c.decode(String.self, forKey: CodingKeys.keyword) + let label = try c.decode(String.self, forKey: CodingKeys.label) + let params = c.contains(CodingKeys.params) ? try c.decode((String?).self, forKey: CodingKeys.params) : nil + self = .command(keyword: keyword, label: label, params: params) + case "menu": + let label = try c.decode(String.self, forKey: CodingKeys.label) + let commands = try c.decode(([ChatBotCommand]).self, forKey: CodingKeys.commands) + self = .menu(label: label, commands: commands) + default: + throw DecodingError.dataCorruptedError(forKey: CodingKeys.type, in: c, debugDescription: "Unsupported command type: \(type)") + } + } +} + +extension ChatBotCommand: Encodable { + public func encode(to encoder: Encoder) throws { + var c = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .command(keyword, label, params): + try c.encode("command", forKey: .type) + try c.encode(keyword, forKey: .keyword) + try c.encode(label, forKey: .label) + if let params { try c.encode(params, forKey: .params) } + case let .menu(label, commands): + try c.encode("menu", forKey: .type) + try c.encode(label, forKey: .label) + try c.encode(commands, forKey: .commands) + } + } +} + public func fullPreferencesToPreferences(_ fullPreferences: FullPreferences) -> Preferences { Preferences( timedMessages: fullPreferences.timedMessages, fullDelete: fullPreferences.fullDelete, reactions: fullPreferences.reactions, voice: fullPreferences.voice, - calls: fullPreferences.calls + files: fullPreferences.files, + calls: fullPreferences.calls, + commands: fullPreferences.commands ) } @@ -346,7 +432,9 @@ public func contactUserPreferencesToPreferences(_ contactUserPreferences: Contac fullDelete: contactUserPreferences.fullDelete.userPreference.preference, reactions: contactUserPreferences.reactions.userPreference.preference, voice: contactUserPreferences.voice.userPreference.preference, - calls: contactUserPreferences.calls.userPreference.preference + files: contactUserPreferences.files.userPreference.preference, + calls: contactUserPreferences.calls.userPreference.preference, + commands: contactUserPreferences.commands ) } @@ -484,20 +572,26 @@ public struct ContactUserPreferences: Decodable, Hashable { public var fullDelete: ContactUserPreference public var reactions: ContactUserPreference public var voice: ContactUserPreference + public var files: ContactUserPreference public var calls: ContactUserPreference + public var commands: [ChatBotCommand]? public init( timedMessages: ContactUserPreference, fullDelete: ContactUserPreference, reactions: ContactUserPreference, voice: ContactUserPreference, - calls: ContactUserPreference + files: ContactUserPreference, + calls: ContactUserPreference, + commands: [ChatBotCommand]? ) { self.timedMessages = timedMessages self.fullDelete = fullDelete self.reactions = reactions self.voice = voice + self.files = files self.calls = calls + self.commands = commands } public static let sampleData = ContactUserPreferences( @@ -521,11 +615,17 @@ public struct ContactUserPreferences: Decodable, Hashable { userPreference: ContactUserPref.user(preference: SimplePreference(allow: .yes)), contactPreference: SimplePreference(allow: .yes) ), + files: ContactUserPreference( + enabled: FeatureEnabled(forUser: true, forContact: true), + userPreference: ContactUserPref.user(preference: SimplePreference(allow: .yes)), + contactPreference: SimplePreference(allow: .yes) + ), calls: ContactUserPreference( enabled: FeatureEnabled(forUser: true, forContact: true), userPreference: ContactUserPref.user(preference: SimplePreference(allow: .yes)), contactPreference: SimplePreference(allow: .yes) - ) + ), + commands: nil ) } @@ -598,6 +698,7 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case fullDelete case reactions case voice + case files case calls public var id: Self { self } @@ -624,6 +725,7 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case .fullDelete: return NSLocalizedString("Delete for everyone", comment: "chat feature") case .reactions: return NSLocalizedString("Message reactions", comment: "chat feature") case .voice: return NSLocalizedString("Voice messages", comment: "chat feature") + case .files: return NSLocalizedString("Files and media", comment: "chat feature") case .calls: return NSLocalizedString("Audio/video calls", comment: "chat feature") } } @@ -634,6 +736,7 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case .fullDelete: return "trash.slash" case .reactions: return "face.smiling" case .voice: return "mic" + case .files: return "doc" case .calls: return "phone" } } @@ -644,6 +747,7 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case .fullDelete: return "trash.slash.fill" case .reactions: return "face.smiling.fill" case .voice: return "mic.fill" + case .files: return "doc.fill" case .calls: return "phone.fill" } } @@ -681,6 +785,12 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case .yes: return "Allow voice messages only if your contact allows them." case .no: return "Prohibit sending voice messages." } + case .files: + switch allowed { + case .always: return "Allow your contacts to send files and media." + case .yes: return "Allow files and media only if your contact allows them." + case .no: return "Prohibit sending files and media." + } case .calls: switch allowed { case .always: return "Allow your contacts to call you." @@ -724,6 +834,14 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { : enabled.forContact ? "Only your contact can send voice messages." : "Voice messages are prohibited in this chat." + case .files: + return enabled.forUser && enabled.forContact + ? "Both you and your contact can send files and media." + : enabled.forUser + ? "Only you can send files and media." + : enabled.forContact + ? "Only your contact can send files and media." + : "Files and media are prohibited in this chat." case .calls: return enabled.forUser && enabled.forContact ? "Both you and your contact can make calls." @@ -957,7 +1075,9 @@ public struct ContactFeaturesAllowed: Equatable, Hashable { public var fullDelete: ContactFeatureAllowed public var reactions: ContactFeatureAllowed public var voice: ContactFeatureAllowed + public var files: ContactFeatureAllowed public var calls: ContactFeatureAllowed + public var commands: [ChatBotCommand]? public init( timedMessagesAllowed: Bool, @@ -965,14 +1085,18 @@ public struct ContactFeaturesAllowed: Equatable, Hashable { fullDelete: ContactFeatureAllowed, reactions: ContactFeatureAllowed, voice: ContactFeatureAllowed, - calls: ContactFeatureAllowed + files: ContactFeatureAllowed, + calls: ContactFeatureAllowed, + commands: [ChatBotCommand]? ) { self.timedMessagesAllowed = timedMessagesAllowed self.timedMessagesTTL = timedMessagesTTL self.fullDelete = fullDelete self.reactions = reactions self.voice = voice + self.files = files self.calls = calls + self.commands = commands } public static let sampleData = ContactFeaturesAllowed( @@ -981,7 +1105,9 @@ public struct ContactFeaturesAllowed: Equatable, Hashable { fullDelete: ContactFeatureAllowed.userDefault(.no), reactions: ContactFeatureAllowed.userDefault(.yes), voice: ContactFeatureAllowed.userDefault(.yes), - calls: ContactFeatureAllowed.userDefault(.yes) + files: ContactFeatureAllowed.userDefault(.always), + calls: ContactFeatureAllowed.userDefault(.yes), + commands: nil ) } @@ -994,7 +1120,9 @@ public func contactUserPrefsToFeaturesAllowed(_ contactUserPreferences: ContactU fullDelete: contactUserPrefToFeatureAllowed(contactUserPreferences.fullDelete), reactions: contactUserPrefToFeatureAllowed(contactUserPreferences.reactions), voice: contactUserPrefToFeatureAllowed(contactUserPreferences.voice), - calls: contactUserPrefToFeatureAllowed(contactUserPreferences.calls) + files: contactUserPrefToFeatureAllowed(contactUserPreferences.files), + calls: contactUserPrefToFeatureAllowed(contactUserPreferences.calls), + commands: contactUserPreferences.commands ) } @@ -1016,7 +1144,9 @@ public func contactFeaturesAllowedToPrefs(_ contactFeaturesAllowed: ContactFeatu fullDelete: contactFeatureAllowedToPref(contactFeaturesAllowed.fullDelete), reactions: contactFeatureAllowedToPref(contactFeaturesAllowed.reactions), voice: contactFeatureAllowedToPref(contactFeaturesAllowed.voice), - calls: contactFeatureAllowedToPref(contactFeaturesAllowed.calls) + files: contactFeatureAllowedToPref(contactFeaturesAllowed.files), + calls: contactFeatureAllowedToPref(contactFeaturesAllowed.calls), + commands: contactFeaturesAllowed.commands ) } @@ -1057,6 +1187,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { public var simplexLinks: RoleGroupPreference public var reports: GroupPreference public var history: GroupPreference + public var commands: [ChatBotCommand] public init( timedMessages: TimedMessagesGroupPreference, @@ -1067,7 +1198,8 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { files: RoleGroupPreference, simplexLinks: RoleGroupPreference, reports: GroupPreference, - history: GroupPreference + history: GroupPreference, + commands: [ChatBotCommand] ) { self.timedMessages = timedMessages self.directMessages = directMessages @@ -1078,6 +1210,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { self.simplexLinks = simplexLinks self.reports = reports self.history = history + self.commands = commands } public static let sampleData = FullGroupPreferences( @@ -1089,7 +1222,8 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { files: RoleGroupPreference(enable: .on, role: nil), simplexLinks: RoleGroupPreference(enable: .on, role: nil), reports: GroupPreference(enable: .on), - history: GroupPreference(enable: .on) + history: GroupPreference(enable: .on), + commands: [] ) } @@ -1103,6 +1237,7 @@ public struct GroupPreferences: Codable, Hashable { public var simplexLinks: RoleGroupPreference? public var reports: GroupPreference? public var history: GroupPreference? + public var commands: [ChatBotCommand]? public init( timedMessages: TimedMessagesGroupPreference? = nil, @@ -1113,7 +1248,8 @@ public struct GroupPreferences: Codable, Hashable { files: RoleGroupPreference? = nil, simplexLinks: RoleGroupPreference? = nil, reports: GroupPreference? = nil, - history: GroupPreference? = nil + history: GroupPreference? = nil, + commands: [ChatBotCommand]? = nil ) { self.timedMessages = timedMessages self.directMessages = directMessages @@ -1124,6 +1260,7 @@ public struct GroupPreferences: Codable, Hashable { self.simplexLinks = simplexLinks self.reports = reports self.history = history + self.commands = commands } public static let sampleData = GroupPreferences( @@ -1135,7 +1272,8 @@ public struct GroupPreferences: Codable, Hashable { files: RoleGroupPreference(enable: .on, role: nil), simplexLinks: RoleGroupPreference(enable: .on, role: nil), reports: GroupPreference(enable: .on), - history: GroupPreference(enable: .on) + history: GroupPreference(enable: .on), + commands: nil ) } @@ -1149,7 +1287,8 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group files: fullPreferences.files, simplexLinks: fullPreferences.simplexLinks, reports: fullPreferences.reports, - history: fullPreferences.history + history: fullPreferences.history, + commands: fullPreferences.commands ) } @@ -1368,6 +1507,19 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } + public var sndReady: Bool { + switch self { + case let .direct(contact): contact.sndReady + case let .group(groupInfo, groupScope): + groupInfo.membership.memberActive + && (groupScope != nil || (!groupInfo.membership.memberPending && groupInfo.membership.memberRole != .observer)) + case .local: true + case .contactRequest: false + case .contactConnection: false + case .invalidJSON: false + } + } + public var chatDeleted: Bool { get { switch self { @@ -1502,6 +1654,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case .fullDelete: return cups.fullDelete.enabled.forUser case .reactions: return cups.reactions.enabled.forUser case .voice: return cups.voice.enabled.forUser + case .files: return cups.files.enabled.forUser case .calls: return cups.calls.enabled.forUser } case let .group(groupInfo, _): @@ -1511,12 +1664,17 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case .fullDelete: return prefs.fullDelete.on case .reactions: return prefs.reactions.on case .voice: return prefs.voice.on(for: groupInfo.membership) + case .files: return prefs.files.on(for: groupInfo.membership) case .calls: return false } case .local: switch feature { + case .timedMessages: return false + case .fullDelete: return false + case .reactions: return false case .voice: return true - default: return false + case .files: return true + case .calls: return false } default: return false } @@ -1619,6 +1777,22 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { if case .group = self { true } else { false } } + public var useCommands: Bool { + switch self { + case let .direct(c): c.isBot + case let .group(g, _): (g.groupProfile.groupPreferences?.commands?.count ?? 0) > 0 + default: false + } + } + + public var menuCommands: [ChatBotCommand] { + switch self { + case let .direct(c): c.isBot ? c.profile.preferences?.commands ?? [] : [] + case let .group(g, _): g.groupProfile.groupPreferences?.commands ?? [] + default: [] + } + } + public var chatTags: [Int64]? { switch self { case let .direct(contact): return contact.chatTags @@ -1803,16 +1977,26 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { (activeConn == nil || activeConn?.connStatus == .prepared) && profile.contactLink != nil && active && preparedContact == nil && contactRequestId == nil } + @inline(__always) + public var isBot: Bool { + profile.peerType == .bot + } + public var contactConnIncognito: Bool { activeConn?.customUserProfileId != nil } + public var chatIconName: String { + isBot ? "cube.fill" : "person.crop.circle.fill" + } + public func allowsFeature(_ feature: ChatFeature) -> Bool { switch feature { case .timedMessages: return mergedPreferences.timedMessages.contactPreference.allow != .no case .fullDelete: return mergedPreferences.fullDelete.contactPreference.allow != .no case .reactions: return mergedPreferences.reactions.contactPreference.allow != .no case .voice: return mergedPreferences.voice.contactPreference.allow != .no + case .files: return mergedPreferences.files.contactPreference.allow != .no case .calls: return mergedPreferences.calls.contactPreference.allow != .no } } @@ -1823,6 +2007,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { case .fullDelete: return mergedPreferences.fullDelete.userPreference.preference.allow != .no case .reactions: return mergedPreferences.reactions.userPreference.preference.allow != .no case .voice: return mergedPreferences.voice.userPreference.preference.allow != .no + case .files: return mergedPreferences.files.userPreference.preference.allow != .no case .calls: return mergedPreferences.calls.userPreference.preference.allow != .no } } @@ -4446,6 +4631,7 @@ public enum Format: Decodable, Equatable, Hashable { case colored(color: FormatColor) case uri case simplexLink(linkType: SimplexLinkType, simplexUri: String, smpHosts: [String]) + case command(commandStr: String) case mention(memberName: String) case email case phone diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift index 2016094958..98ee9cd5d4 100644 --- a/apps/ios/SimpleXChat/ChatUtils.swift +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -93,7 +93,7 @@ private func canForwardToChat(_ cInfo: ChatInfo) -> Bool { public func chatIconName(_ cInfo: ChatInfo) -> String { switch cInfo { - case .direct: "person.crop.circle.fill" + case let .direct(contact): contact.chatIconName case let .group(groupInfo, _): groupInfo.chatIconName case .local: "folder.circle.fill" case .contactRequest: "person.crop.circle.fill" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index d61e44f528..962bd82dd3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1633,6 +1633,18 @@ sealed class ChatInfo: SomeChat, NamedChat { val sendMsgEnabled get() = userCantSendReason == null + val sndReady: Boolean get() = + when(this) { + is Direct -> contact.sndReady + is Group -> + groupInfo.membership.memberActive + && (groupChatScope != null || (!groupInfo.membership.memberPending && groupInfo.membership.memberRole != GroupMemberRole.Observer)) + is Local -> true + is ContactRequest -> false + is ContactConnection -> false + is InvalidJSON -> false + } + fun groupChatScope(): GroupChatScope? = when (this) { is Group -> groupChatScope?.toChatScope() else -> null @@ -1676,6 +1688,20 @@ sealed class ChatInfo: SomeChat, NamedChat { val hasMentions: Boolean get() = this is Group + val useCommands: Boolean get() = when(this) { + is Direct -> contact.isBot + is Group -> groupInfo.groupProfile.groupPreferences?.commands?.isNotEmpty() ?: false + else -> false + } + + val menuCommands: List get() = when(this) { + is Direct -> + if (contact.isBot) contact.profile.preferences?.commands ?: emptyList() + else emptyList() + is Group -> groupInfo.groupProfile.groupPreferences?.commands ?: emptyList() + else -> emptyList() + } + val contactCard: Boolean get() = when (this) { is Direct -> contact.isContactCard @@ -1759,6 +1785,7 @@ data class Contact( ChatFeature.FullDelete -> mergedPreferences.fullDelete.enabled.forUser ChatFeature.Reactions -> mergedPreferences.reactions.enabled.forUser ChatFeature.Voice -> mergedPreferences.voice.enabled.forUser + ChatFeature.Files -> mergedPreferences.files.enabled.forUser ChatFeature.Calls -> mergedPreferences.calls.enabled.forUser } override val timedMessagesTTL: Int? get() = with(mergedPreferences.timedMessages) { if (enabled.forUser) userPreference.pref.ttl else null } @@ -1775,7 +1802,6 @@ data class Contact( return profile.chatViewName.lowercase().contains(s) || profile.displayName.lowercase().contains(s) || profile.fullName.lowercase().contains(s) } - val directOrUsed: Boolean get() = if (activeConn != null) { (activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed @@ -1783,16 +1809,22 @@ data class Contact( true } - val isContactCard: Boolean = + val isContactCard: Boolean get() = (activeConn == null || activeConn.connStatus == ConnStatus.Prepared) && profile.contactLink != null && active && preparedContact == null && contactRequestId == null - val contactConnIncognito = + val isBot: Boolean get() = profile.peerType == ChatPeerType.Bot + + val contactConnIncognito: Boolean get() = activeConn?.customUserProfileId != null + val chatIconName: ImageResource get() = + if (isBot) MR.images.ic_cube else MR.images.ic_account_circle_filled + fun allowsFeature(feature: ChatFeature): Boolean = when (feature) { ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO ChatFeature.FullDelete -> mergedPreferences.fullDelete.contactPreference.allow != FeatureAllowed.NO ChatFeature.Voice -> mergedPreferences.voice.contactPreference.allow != FeatureAllowed.NO + ChatFeature.Files -> mergedPreferences.files.contactPreference.allow != FeatureAllowed.NO ChatFeature.Reactions -> mergedPreferences.reactions.contactPreference.allow != FeatureAllowed.NO ChatFeature.Calls -> mergedPreferences.calls.contactPreference.allow != FeatureAllowed.NO } @@ -1802,6 +1834,7 @@ data class Contact( ChatFeature.FullDelete -> mergedPreferences.fullDelete.userPreference.pref.allow != FeatureAllowed.NO ChatFeature.Reactions -> mergedPreferences.reactions.userPreference.pref.allow != FeatureAllowed.NO ChatFeature.Voice -> mergedPreferences.voice.userPreference.pref.allow != FeatureAllowed.NO + ChatFeature.Files -> mergedPreferences.files.userPreference.pref.allow != FeatureAllowed.NO ChatFeature.Calls -> mergedPreferences.calls.userPreference.pref.allow != FeatureAllowed.NO } @@ -1931,14 +1964,15 @@ data class Profile( override val image: String? = null, override val localAlias : String = "", val contactLink: String? = null, - val preferences: ChatPreferences? = null + val preferences: ChatPreferences? = null, + val peerType: ChatPeerType? = null ): NamedChat { val profileViewName: String get() { return if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } - fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, shortDescr, image, localAlias, contactLink, preferences) + fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType) companion object { val sampleData = Profile( @@ -1958,11 +1992,12 @@ data class LocalProfile( override val image: String? = null, override val localAlias: String, val contactLink: String? = null, - val preferences: ChatPreferences? = null + val preferences: ChatPreferences? = null, + val peerType: ChatPeerType? = null ): NamedChat { val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } - fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, image, localAlias, contactLink, preferences) + fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType) companion object { val sampleData = LocalProfile( @@ -1976,6 +2011,12 @@ data class LocalProfile( } } +@Serializable +enum class ChatPeerType { + @SerialName("human") Human, + @SerialName("bot") Bot +} + @Serializable data class UserProfileUpdateSummary( val updateSuccesses: Int, @@ -2030,6 +2071,7 @@ data class GroupInfo ( ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on ChatFeature.Reactions -> fullGroupPreferences.reactions.on ChatFeature.Voice -> fullGroupPreferences.voice.on(membership) + ChatFeature.Files -> fullGroupPreferences.files.on(membership) ChatFeature.Calls -> false } override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null } @@ -2484,7 +2526,14 @@ class NoteFolder( override val nextConnectPrepared get() = false override val profileChangeProhibited get() = false override val incognito get() = false - override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice + override fun featureEnabled(feature: ChatFeature) = when (feature) { + ChatFeature.TimedMessages -> false + ChatFeature.FullDelete -> false + ChatFeature.Reactions -> false + ChatFeature.Voice -> true + ChatFeature.Files -> true + ChatFeature.Calls -> false + } override val timedMessagesTTL: Int? get() = null override val displayName get() = generalGetString(MR.strings.note_folder_local_display_name) override val fullName get() = "" @@ -4349,6 +4398,7 @@ sealed class Format { @Serializable @SerialName("colored") class Colored(val color: FormatColor): Format() @Serializable @SerialName("uri") class Uri: Format() @Serializable @SerialName("simplexLink") class SimplexLink(val linkType: SimplexLinkType, val simplexUri: String, val smpHosts: List): Format() + @Serializable @SerialName("command") class Command(val commandStr: String): Format() @Serializable @SerialName("mention") class Mention(val memberName: String): Format() @Serializable @SerialName("email") class Email: Format() @Serializable @SerialName("phone") class Phone: Format() @@ -4363,6 +4413,7 @@ sealed class Format { is Colored -> SpanStyle(color = this.color.uiColor) is Uri -> linkStyle is SimplexLink -> linkStyle + is Command -> SpanStyle(color = MaterialTheme.colors.primary, fontFamily = FontFamily.Monospace) is Mention -> SpanStyle(fontWeight = FontWeight.Medium) is Email -> linkStyle is Phone -> linkStyle diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 9e10d249c0..b23869849d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.setNetCfg import chat.simplex.common.model.ChatModel.changingActiveUserMutex +import chat.simplex.common.model.GroupFeature.Files import chat.simplex.common.model.MsgContent.MCUnknown import chat.simplex.common.model.SMPProxyFallback.AllowProtected import chat.simplex.common.model.SMPProxyMode.Always @@ -2643,8 +2644,6 @@ object ChatController { if (cItem.isActiveReport) { chatModel.chatsContext.increaseGroupReportsCounter(rhId, cInfo.id) } - } - withContext(Dispatchers.Main) { chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem) } } else if (cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) { @@ -4881,14 +4880,18 @@ data class FullChatPreferences( val fullDelete: SimpleChatPreference, val reactions: SimpleChatPreference, val voice: SimpleChatPreference, + val files: SimpleChatPreference, val calls: SimpleChatPreference, + val commands: List ) { fun toPreferences(): ChatPreferences = ChatPreferences( timedMessages = timedMessages, fullDelete = fullDelete, reactions = reactions, voice = voice, - calls = calls + files = files, + calls = calls, + commands = commands, ) companion object { @@ -4897,7 +4900,9 @@ data class FullChatPreferences( fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO), reactions = SimpleChatPreference(allow = FeatureAllowed.YES), voice = SimpleChatPreference(allow = FeatureAllowed.YES), + files = SimpleChatPreference(allow = FeatureAllowed.YES), calls = SimpleChatPreference(allow = FeatureAllowed.YES), + commands = listOf(), ) } } @@ -4908,7 +4913,9 @@ data class ChatPreferences( val fullDelete: SimpleChatPreference?, val reactions: SimpleChatPreference?, val voice: SimpleChatPreference?, + val files: SimpleChatPreference?, val calls: SimpleChatPreference?, + val commands: List?, ) { fun setAllowed(feature: ChatFeature, allowed: FeatureAllowed = FeatureAllowed.YES, param: Int? = null): ChatPreferences = when (feature) { @@ -4916,6 +4923,7 @@ data class ChatPreferences( ChatFeature.FullDelete -> this.copy(fullDelete = SimpleChatPreference(allow = allowed)) ChatFeature.Reactions -> this.copy(reactions = SimpleChatPreference(allow = allowed)) ChatFeature.Voice -> this.copy(voice = SimpleChatPreference(allow = allowed)) + ChatFeature.Files -> this.copy(files = SimpleChatPreference(allow = allowed)) ChatFeature.Calls -> this.copy(calls = SimpleChatPreference(allow = allowed)) } @@ -4925,7 +4933,9 @@ data class ChatPreferences( fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO), reactions = SimpleChatPreference(allow = FeatureAllowed.YES), voice = SimpleChatPreference(allow = FeatureAllowed.YES), + files = SimpleChatPreference(allow = FeatureAllowed.YES), calls = SimpleChatPreference(allow = FeatureAllowed.YES), + commands = null, ) } } @@ -4953,6 +4963,12 @@ data class TimedMessagesPreference( } } +@Serializable +sealed class ChatBotCommand { + @Serializable @SerialName("command") class Command(val keyword: String, val label: String, val params: String?): ChatBotCommand() + @Serializable @SerialName("menu") class Menu(val label: String, val commands: List): ChatBotCommand() +} + @Serializable data class PresentedServersSummary( val statsStartedAt: Instant, @@ -5203,14 +5219,18 @@ data class ContactUserPreferences( val fullDelete: ContactUserPreference, val reactions: ContactUserPreference, val voice: ContactUserPreference, + val files: ContactUserPreference, val calls: ContactUserPreference, + val commands: List?, ) { fun toPreferences(): ChatPreferences = ChatPreferences( timedMessages = timedMessages.userPreference.pref, fullDelete = fullDelete.userPreference.pref, reactions = reactions.userPreference.pref, voice = voice.userPreference.pref, - calls = calls.userPreference.pref + files = files.userPreference.pref, + calls = calls.userPreference.pref, + commands = commands, ) companion object { @@ -5235,11 +5255,17 @@ data class ContactUserPreferences( userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)), contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES) ), + files = ContactUserPreference( + enabled = FeatureEnabled(forUser = true, forContact = true), + userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)), + contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES) + ), calls = ContactUserPreference( enabled = FeatureEnabled(forUser = true, forContact = true), userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)), contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES) ), + commands = null, ) } } @@ -5329,6 +5355,7 @@ enum class ChatFeature: Feature { @SerialName("fullDelete") FullDelete, @SerialName("reactions") Reactions, @SerialName("voice") Voice, + @SerialName("files") Files, @SerialName("calls") Calls; val asymmetric: Boolean get() = when (this) { @@ -5348,6 +5375,7 @@ enum class ChatFeature: Feature { FullDelete -> generalGetString(MR.strings.full_deletion) Reactions -> generalGetString(MR.strings.message_reactions) Voice -> generalGetString(MR.strings.voice_messages) + Files -> generalGetString(MR.strings.files_and_media) Calls -> generalGetString(MR.strings.audio_video_calls) } @@ -5357,6 +5385,7 @@ enum class ChatFeature: Feature { FullDelete -> painterResource(MR.images.ic_delete_forever) Reactions -> painterResource(MR.images.ic_add_reaction) Voice -> painterResource(MR.images.ic_keyboard_voice) + Files -> painterResource(MR.images.ic_draft) Calls -> painterResource(MR.images.ic_call) } @@ -5366,6 +5395,7 @@ enum class ChatFeature: Feature { FullDelete -> painterResource(MR.images.ic_delete_forever_filled) Reactions -> painterResource(MR.images.ic_add_reaction_filled) Voice -> painterResource(MR.images.ic_keyboard_voice_filled) + Files -> painterResource(MR.images.ic_draft_filled) Calls -> painterResource(MR.images.ic_call_filled) } @@ -5386,11 +5416,16 @@ enum class ChatFeature: Feature { FeatureAllowed.YES -> generalGetString(MR.strings.allow_message_reactions_only_if) FeatureAllowed.NO -> generalGetString(MR.strings.prohibit_message_reactions) } - Voice -> when (allowed) { + Voice -> when (allowed) { FeatureAllowed.ALWAYS -> generalGetString(MR.strings.allow_your_contacts_to_send_voice_messages) FeatureAllowed.YES -> generalGetString(MR.strings.allow_voice_messages_only_if) FeatureAllowed.NO -> generalGetString(MR.strings.prohibit_sending_voice_messages) } + Files -> when (allowed) { + FeatureAllowed.ALWAYS -> generalGetString(MR.strings.allow_your_contacts_to_send_files_and_media) + FeatureAllowed.YES -> generalGetString(MR.strings.allow_files_and_media_only_if) + FeatureAllowed.NO -> generalGetString(MR.strings.prohibit_sending_files_and_media) + } Calls -> when (allowed) { FeatureAllowed.ALWAYS -> generalGetString(MR.strings.allow_your_contacts_to_call) FeatureAllowed.YES -> generalGetString(MR.strings.allow_calls_only_if) @@ -5424,6 +5459,12 @@ enum class ChatFeature: Feature { enabled.forContact -> generalGetString(MR.strings.only_your_contact_can_send_voice) else -> generalGetString(MR.strings.voice_prohibited_in_this_chat) } + Files -> when { + enabled.forUser && enabled.forContact -> generalGetString(MR.strings.both_you_and_your_contact_can_send_files) + enabled.forUser -> generalGetString(MR.strings.only_you_can_send_files) + enabled.forContact -> generalGetString(MR.strings.only_your_contact_can_send_files) + else -> generalGetString(MR.strings.files_prohibited_in_this_chat) + } Calls -> when { enabled.forUser && enabled.forContact -> generalGetString(MR.strings.both_you_and_your_contact_can_make_calls) enabled.forUser -> generalGetString(MR.strings.only_you_can_make_calls) @@ -5618,7 +5659,9 @@ data class ContactFeaturesAllowed( val fullDelete: ContactFeatureAllowed, val reactions: ContactFeatureAllowed, val voice: ContactFeatureAllowed, + val files: ContactFeatureAllowed, val calls: ContactFeatureAllowed, + val commands: List?, ) { companion object { val sampleData = ContactFeaturesAllowed( @@ -5627,7 +5670,9 @@ data class ContactFeaturesAllowed( fullDelete = ContactFeatureAllowed.UserDefault(FeatureAllowed.NO), reactions = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES), voice = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES), + files = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES), calls = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES), + commands = null, ) } } @@ -5641,7 +5686,9 @@ fun contactUserPrefsToFeaturesAllowed(contactUserPreferences: ContactUserPrefere fullDelete = contactUserPrefToFeatureAllowed(contactUserPreferences.fullDelete), reactions = contactUserPrefToFeatureAllowed(contactUserPreferences.reactions), voice = contactUserPrefToFeatureAllowed(contactUserPreferences.voice), + files = contactUserPrefToFeatureAllowed(contactUserPreferences.files), calls = contactUserPrefToFeatureAllowed(contactUserPreferences.calls), + commands = contactUserPreferences.commands, ) } @@ -5661,7 +5708,9 @@ fun contactFeaturesAllowedToPrefs(contactFeaturesAllowed: ContactFeaturesAllowed fullDelete = contactFeatureAllowedToPref(contactFeaturesAllowed.fullDelete), reactions = contactFeatureAllowedToPref(contactFeaturesAllowed.reactions), voice = contactFeatureAllowedToPref(contactFeaturesAllowed.voice), + files = contactFeatureAllowedToPref(contactFeaturesAllowed.files), calls = contactFeatureAllowedToPref(contactFeaturesAllowed.calls), + commands = contactFeaturesAllowed.commands, ) fun contactFeatureAllowedToPref(contactFeatureAllowed: ContactFeatureAllowed): SimpleChatPreference? = @@ -5697,6 +5746,7 @@ data class FullGroupPreferences( val simplexLinks: RoleGroupPreference, val reports: GroupPreference, val history: GroupPreference, + val commands: List, ) { fun toGroupPreferences(): GroupPreferences = GroupPreferences( @@ -5709,6 +5759,7 @@ data class FullGroupPreferences( simplexLinks = simplexLinks, reports = reports, history = history, + commands = commands, ) companion object { @@ -5722,6 +5773,7 @@ data class FullGroupPreferences( simplexLinks = RoleGroupPreference(GroupFeatureEnabled.ON, role = null), reports = GroupPreference(GroupFeatureEnabled.ON), history = GroupPreference(GroupFeatureEnabled.ON), + commands = listOf() ) } } @@ -5737,6 +5789,7 @@ data class GroupPreferences( val simplexLinks: RoleGroupPreference? = null, val reports: GroupPreference? = null, val history: GroupPreference? = null, + val commands: List? = null ) { companion object { val sampleData = GroupPreferences( @@ -5749,6 +5802,7 @@ data class GroupPreferences( simplexLinks = RoleGroupPreference(GroupFeatureEnabled.ON, role = null), reports = GroupPreference(GroupFeatureEnabled.ON), history = GroupPreference(GroupFeatureEnabled.ON), + commands = null, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index d19c19b83a..a3365b2087 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -102,19 +102,21 @@ fun ChatView( val chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value } // They have their own iterator inside for a reason to prevent crash "Reading a state that was created after the snapshot..." val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } } - val activeChatInfo = remember { derivedStateOf { - var chatInfo = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo + val activeChat = remember { derivedStateOf { + var chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value } + val chatInfo = chat?.chatInfo if ( chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext + && chat != null && chatInfo is ChatInfo.Group ) { val scopeInfo = chatsCtx.secondaryContextFilter.groupScopeInfo - chatInfo = chatInfo.copy(groupChatScope = scopeInfo) + chat = chat.copy(chatInfo = chatInfo.copy(groupChatScope = scopeInfo)) } - chatInfo + chat } } val user = chatModel.currentUser.value - val chatInfo = activeChatInfo.value + val chatInfo = activeChat.value?.chatInfo if (chat == null || chatInfo == null || user == null) { LaunchedEffect(Unit) { chatModel.chatId.value = null @@ -138,6 +140,7 @@ fun ChatView( val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() val selectedChatItems = rememberSaveable { mutableStateOf(null as Set?) } + val showCommandsMenu = rememberSaveable { mutableStateOf(false) } if (appPlatform.isAndroid) { DisposableEffect(Unit) { onDispose { @@ -186,7 +189,6 @@ fun ChatView( chatsCtx.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0 } } - val reportsCount = reportsCount(chatInfo.id) val clipboard = LocalClipboardManager.current CompositionLocalProvider( LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false), @@ -214,7 +216,7 @@ fun ChatView( ChatLayout( chatsCtx = chatsCtx, remoteHostId = remoteHostId, - chatInfo = activeChatInfo, + chat = activeChat, unreadCount, composeState, composeView = { focusRequester -> @@ -237,7 +239,7 @@ fun ChatView( ) } ComposeView( - rhId = remoteHostId.value, chatModel, chatsCtx, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption, + rhId = remoteHostId.value, chatModel, chatsCtx, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, showCommandsMenu, attachmentOption, showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }, focusRequester = focusRequester ) @@ -347,7 +349,7 @@ fun ChatView( ModalManager.end.showCustomModal { close -> val appBar = remember { mutableStateOf(null as @Composable (BoxScope.() -> Unit)?) } ModalView(close, appBar = appBar.value) { - val chatInfo = remember { activeChatInfo }.value + val chatInfo = remember { activeChat }.value?.chatInfo if (chatInfo is ChatInfo.Direct) { var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } var code: String? by remember { mutableStateOf(preloadedCode) } @@ -377,7 +379,7 @@ fun ChatView( } } LaunchedEffect(Unit) { - snapshotFlow { activeChatInfo.value?.id } + snapshotFlow { activeChat.value?.id } .drop(1) .collect { appBar.value = null @@ -389,37 +391,37 @@ fun ChatView( } }, showReports = { - val info = activeChatInfo.value ?: return@ChatLayout + val cInfo = activeChat.value?.chatInfo ?: return@ChatLayout if (ModalManager.end.hasModalsOpen()) { ModalManager.end.closeModals() return@ChatLayout } hideKeyboard(view) scope.launch { - showGroupReportsView(staleChatId, scrollToItemId, info) + showGroupReportsView(staleChatId, scrollToItemId, cInfo) } }, showSupportChats = { - val info = activeChatInfo.value ?: return@ChatLayout + val cInfo = activeChat.value?.chatInfo ?: return@ChatLayout if (ModalManager.end.hasModalsOpen()) { ModalManager.end.closeModals() return@ChatLayout } hideKeyboard(view) scope.launch { - if (info is ChatInfo.Group && info.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + if (cInfo is ChatInfo.Group && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { ModalManager.end.showCustomModal { close -> MemberSupportView( chatRh, chat, - info.groupInfo, + cInfo.groupInfo, scrollToItemId, close ) } - } else if (info is ChatInfo.Group) { + } else if (cInfo is ChatInfo.Group) { val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null) - val supportChatInfo = ChatInfo.Group(info.groupInfo, groupChatScope = scopeInfo) + val supportChatInfo = ChatInfo.Group(cInfo.groupInfo, groupChatScope = scopeInfo) scope.launch { showMemberSupportChatView( chatModel.chatId, @@ -723,7 +725,8 @@ fun ChatView( onComposed, developerTools = chatModel.controller.appPrefs.developerTools.get(), showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), - showSearch = showSearch + showSearch = showSearch, + showCommandsMenu = showCommandsMenu ) } } @@ -768,9 +771,7 @@ private fun connectingText(chatInfo: ChatInfo): String? { && !chatInfo.contact.sendMsgToConnect && !chatInfo.contact.nextAcceptContactRequest ) { - if (chatInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con) { - generalGetString(MR.strings.contact_should_accept) - } else if (chatInfo.contact.contactGroupMemberId != null) { + if ((chatInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con && !chatInfo.contact.isBot) || chatInfo.contact.contactGroupMemberId != null) { generalGetString(MR.strings.contact_should_accept) } else { generalGetString(MR.strings.contact_connection_pending) @@ -807,7 +808,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) fun ChatLayout( chatsCtx: ChatModel.ChatsContext, remoteHostId: State, - chatInfo: State, + chat: State, unreadCount: State, composeState: MutableState, composeView: (@Composable (FocusRequester?) -> Unit), @@ -854,8 +855,10 @@ fun ChatLayout( onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, showViaProxy: Boolean, - showSearch: MutableState + showSearch: MutableState, + showCommandsMenu: MutableState ) { + val chatInfo = remember { derivedStateOf { chat.value?.chatInfo } } val scope = rememberCoroutineScope() val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } } Box( @@ -886,19 +889,20 @@ fun ChatLayout( val composeViewHeight = remember { mutableStateOf(0.dp) } Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, drawWallpaper = chatsCtx.secondaryContextFilter == null)) { val remoteHostId = remember { remoteHostId }.value - val chatInfo = remember { chatInfo }.value + val chat = remember { chat }.value + val chatInfo = chat?.chatInfo val oneHandUI = remember { appPrefs.oneHandUI.state } val chatBottomBar = remember { appPrefs.chatBottomBar.state } val composeViewFocusRequester = remember { if (appPlatform.isDesktop) FocusRequester() else null } AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) { - if (chatInfo != null) { + if (chat != null) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { // disables scrolling to top of chat item on click inside the bubble CompositionLocalProvider(LocalBringIntoViewSpec provides object : BringIntoViewSpec { override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f }) { ChatItemsList( - chatsCtx, remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, + chatsCtx, remoteHostId, chat, unreadCount, composeState, composeViewHeight, searchValue, useLinkPreviews, linkMode, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, archiveReports, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, @@ -920,6 +924,11 @@ fun ChatLayout( ) } } + if (chatInfo != null && chatInfo.menuCommands.isNotEmpty()) { + Column(Modifier.align(Alignment.BottomStart).padding(bottom = composeViewHeight.value)) { + CommandsMenuView(chatsCtx, chat, composeState, showCommandsMenu) + } + } } } if (chatsCtx.contentTag == MsgContentTag.Report) { @@ -1362,7 +1371,7 @@ private var reportsListState: LazyListState? = null fun BoxScope.ChatItemsList( chatsCtx: ChatModel.ChatsContext, remoteHostId: Long?, - chatInfo: ChatInfo, + chat: Chat, unreadCount: State, composeState: MutableState, composeViewHeight: State, @@ -1399,6 +1408,7 @@ fun BoxScope.ChatItemsList( developerTools: Boolean, showViaProxy: Boolean ) { + val chatInfo = chat.chatInfo val loadingTopItems = remember { mutableStateOf(false) } val loadingBottomItems = remember { mutableStateOf(false) } // just for changing local var here based on request @@ -1561,7 +1571,7 @@ fun BoxScope.ChatItemsList( highlightedItems.value = setOf() } } - ChatItemView(chatsCtx, remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToItemId = scrollToItemId, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + ChatItemView(chatsCtx, remoteHostId, chat, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToItemId = scrollToItemId, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @@ -1769,10 +1779,12 @@ fun BoxScope.ChatItemsList( if (contact.nextConnectPrepared && preparedLinkType != null) { when (preparedLinkType) { ConnectionMode.Inv -> generalGetString(MR.strings.chat_banner_connect_to_chat) - ConnectionMode.Con -> generalGetString(MR.strings.chat_banner_send_request_to_connect) + ConnectionMode.Con -> generalGetString(if (contact.isBot) MR.strings.chat_banner_connect_to_use_bot else MR.strings.chat_banner_send_request_to_connect) } } else if (contact.nextAcceptContactRequest) { generalGetString(MR.strings.chat_banner_accept_contact_request) + } else if (contact.isBot) { + generalGetString(MR.strings.chat_banner_bot) } else { generalGetString(MR.strings.chat_banner_your_contact) } @@ -3304,7 +3316,7 @@ fun PreviewChatLayout() { ChatLayout( chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), remoteHostId = remember { mutableStateOf(null) }, - chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, + chat = remember { mutableStateOf(Chat.sampleData) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = { _ -> }, @@ -3351,7 +3363,8 @@ fun PreviewChatLayout() { onComposed = {}, developerTools = false, showViaProxy = false, - showSearch = remember { mutableStateOf(false) } + showSearch = remember { mutableStateOf(false) }, + showCommandsMenu = remember { mutableStateOf(false) } ) } } @@ -3383,7 +3396,7 @@ fun PreviewGroupChatLayout() { ChatLayout( chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), remoteHostId = remember { mutableStateOf(null) }, - chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, + chat = remember { mutableStateOf(Chat.sampleData) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = { _ -> }, @@ -3430,7 +3443,8 @@ fun PreviewGroupChatLayout() { onComposed = {}, developerTools = false, showViaProxy = false, - showSearch = remember { mutableStateOf(false) } + showSearch = remember { mutableStateOf(false) }, + showCommandsMenu = remember { mutableStateOf(false) } ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/CommandsMenuView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/CommandsMenuView.kt new file mode 100644 index 0000000000..26b1fce741 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/CommandsMenuView.kt @@ -0,0 +1,244 @@ +package chat.simplex.common.views.chat + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.views.chat.group.* +import chat.simplex.common.views.chat.item.sendCommandMsg +import chat.simplex.common.views.helpers.commandMenuAnimSpec +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import kotlinx.coroutines.launch + +private val COMMAND_MENU_ROW_SIZE = 48.dp +private val MAX_COMMAND_MENU_HEIGHT = COMMAND_MENU_ROW_SIZE * 6 - 8.dp + +@Composable +fun CommandsMenuView( + chatsCtx: ChatModel.ChatsContext, + chat: Chat, + composeState: MutableState, + showCommandsMenu: MutableState +) { + val maxHeightInPx = with(LocalDensity.current) { windowHeight().toPx() } + val offsetY = remember { Animatable(maxHeightInPx) } + val scope = rememberCoroutineScope() + + val currentCommands = remember { mutableStateOf>(emptyList()) } + val menuTreeBackPath = remember { mutableStateOf>>>(emptyList()) } + + fun filterShownCommands(commands: List, msg: CharSequence): List { + val cmds = mutableListOf() + for (cmd in commands) { + when (cmd) { + is ChatBotCommand.Command -> + if (cmd.keyword.startsWith(msg)) { + cmds.add(cmd) + } + is ChatBotCommand.Menu -> + cmds.addAll(filterShownCommands(cmd.commands, msg)) + } + } + return cmds + } + + suspend fun closeCommandsMenu() { + showCommandsMenu.value = false + currentCommands.value = emptyList() + menuTreeBackPath.value = emptyList() + if (offsetY.value != 0f) { + return + } + offsetY.animateTo( + targetValue = maxHeightInPx, + animationSpec = commandMenuAnimSpec() + ) + } + + fun messageChanged(message: String) { + val msg = message.trim() + menuTreeBackPath.value = emptyList() + if (msg == "/") { + currentCommands.value = chat.chatInfo.menuCommands + } else if (msg.startsWith("/")) { + currentCommands.value = filterShownCommands(chat.chatInfo.menuCommands, msg.drop(1)) + } else { + scope.launch { closeCommandsMenu() } + } + } + + LaunchedEffect(currentCommands.value.isNotEmpty()) { + if (currentCommands.value.isNotEmpty()) { + offsetY.animateTo( + targetValue = 0f, + animationSpec = commandMenuAnimSpec() + ) + } + } + + LaunchedEffect(composeState.value.message) { + messageChanged(composeState.value.message.text) + } + + LaunchedEffect(showCommandsMenu.value) { + if (showCommandsMenu.value) { + currentCommands.value = chat.chatInfo.menuCommands + menuTreeBackPath.value = emptyList() + } else { + closeCommandsMenu() + } + } + + @Composable + fun MenuLabelRow(prev: Pair>) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(COMMAND_MENU_ROW_SIZE) + .clickable { + if (menuTreeBackPath.value.isNotEmpty()) { + currentCommands.value = menuTreeBackPath.value.last().second + menuTreeBackPath.value = menuTreeBackPath.value.dropLast(1) + } + }, + contentAlignment = Alignment.Center + ) { + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Icon( + painterResource(MR.images.ic_arrow_back_ios_new), + contentDescription = null, + tint = MaterialTheme.colors.secondary + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text( + text = prev.first, + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium, + maxLines = 1, + modifier = Modifier.weight(1f), + overflow = TextOverflow.Ellipsis + ) + } + } + } + + @Composable + fun CommandRow(cmd: ChatBotCommand) { + when (cmd) { + is ChatBotCommand.Command -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(COMMAND_MENU_ROW_SIZE) + .clickable { + if (cmd.params != null) { + val msg = "/${cmd.keyword} ${cmd.params}" + composeState.value = ComposeState(message = ComposeMessage(msg, TextRange(msg.length)), useLinkPreviews = true) + } else { + composeState.value = ComposeState(message = ComposeMessage(), useLinkPreviews = true) + sendCommandMsg(chatsCtx, chat,"/${cmd.keyword}") + } + scope.launch { closeCommandsMenu() } + }, + contentAlignment = Alignment.Center + ) { + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Text( + text = cmd.label, + style = MaterialTheme.typography.body1, + maxLines = 1, + modifier = Modifier.weight(1f), + textAlign = TextAlign.Start, + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text( + text = "/${cmd.keyword}", + style = MaterialTheme.typography.body2, + maxLines = 1, + color = MaterialTheme.colors.secondary + ) + } + } + } + is ChatBotCommand.Menu -> + Box( + modifier = Modifier + .fillMaxWidth() + .height(COMMAND_MENU_ROW_SIZE) + .clickable { + menuTreeBackPath.value += Pair(cmd.label, currentCommands.value) + currentCommands.value = cmd.commands + }, + contentAlignment = Alignment.Center + ) { + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Text( + text = cmd.label, + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Medium, + maxLines = 1, + modifier = Modifier.weight(1f), + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Icon( + painterResource(MR.images.ic_chevron_right), + contentDescription = null, + tint = MaterialTheme.colors.secondary + ) + } + } + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .offset { IntOffset(0, offsetY.value.toInt()) } + .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { + scope.launch { closeCommandsMenu() } + }, + contentAlignment = Alignment.BottomStart + ) { + LazyColumnWithScrollBarNoAppBar( + Modifier + .heightIn(max = MAX_COMMAND_MENU_HEIGHT) + .background(MaterialTheme.colors.surface), + maxHeight = remember { mutableStateOf(MAX_COMMAND_MENU_HEIGHT) }, + containerAlignment = Alignment.BottomEnd + ) { + itemsIndexed(currentCommands.value, key = { i, cmd -> "$i ${cmd.hashCode()}" }) { i, command -> + if (i == 0) { + val prev = menuTreeBackPath.value.lastOrNull() + if (prev != null) { + Divider() + MenuLabelRow(prev) + } + } + Divider() + Box(Modifier.fillMaxWidth()) { CommandRow(command) } + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index ce55c62ae2..5f99ac77d6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -17,7 +17,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextDecoration import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp @@ -345,6 +347,7 @@ fun ComposeView( chatsCtx: ChatModel.ChatsContext, chat: Chat, composeState: MutableState, + showCommandsMenu: MutableState, attachmentOption: MutableState, showChooseAttachment: () -> Unit, focusRequester: FocusRequester?, @@ -483,7 +486,7 @@ fun ComposeView( if (!chatItems.isNullOrEmpty()) { chatItems.forEach { aChatItem -> withContext(Dispatchers.Main) { - chatsCtx.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + chatsCtx.addChatItem(chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem) } } return chatItems.first().chatItem @@ -593,7 +596,6 @@ fun ComposeView( } suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): List? { - val cInfo = chat.chatInfo val cs = composeState.value var sent: List? var lastMessageFailedToSend: ComposeState? = null @@ -1064,6 +1066,22 @@ fun ComposeView( val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason) val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) + @Composable + fun CommandsButton() { + val commandsEnabled = chat.chatInfo.sendMsgEnabled && chat.chatInfo.menuCommands.isNotEmpty() + IconButton( + onClick = { showCommandsMenu.value = !showCommandsMenu.value }, + enabled = commandsEnabled + ) { + Box( + modifier = Modifier.size(28.dp).clip(CircleShape), + contentAlignment = Alignment.Center + ) { + Text("//", style = MaterialTheme.typography.h3.copy(fontStyle = FontStyle.Italic, color = if (commandsEnabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary)) + } + } + } + @Composable fun AttachmentButton() { val isGroupAndProhibitedFiles = @@ -1087,7 +1105,6 @@ fun ComposeView( && !nextSendGrpInv.value IconButton( attachmentClicked, - Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), enabled = attachmentEnabled ) { Icon( @@ -1101,6 +1118,24 @@ fun ComposeView( } } + @Composable + fun AttachmentAndCommandsButtons() { + val cInfo = chat.chatInfo + Row( + Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), + horizontalArrangement = Arrangement.spacedBy((-8).dp) + ) { + val msg = composeState.value.message.text.trim() + val showAttachment = cInfo !is ChatInfo.Direct || cInfo.contact.profile.peerType != ChatPeerType.Bot || cInfo.featureEnabled(ChatFeature.Files) + if (cInfo.useCommands && (!showAttachment || msg.isEmpty() || msg.startsWith("/"))) { + CommandsButton() + } + if (showAttachment) { + AttachmentButton() + } + } + } + val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) } LaunchedEffect(allowedVoiceByPrefs) { if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) { @@ -1280,7 +1315,7 @@ fun ComposeView( ) Text( text, - style = MaterialTheme.typography.caption, + style = MaterialTheme.typography.body2, color = if (composeState.value.inProgress) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) } @@ -1433,7 +1468,7 @@ fun ComposeView( ContextSendMessageToConnect(generalGetString(MR.strings.compose_send_direct_message_to_connect)) Divider() Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { - AttachmentButton() + AttachmentAndCommandsButtons() SendMsgView_( disableSendButton = disableSendButton, sendToConnect = { withApi { sendMemberContactInvitation() } } @@ -1453,11 +1488,19 @@ fun ComposeView( connect = { withApi { sendConnectPreparedContact() } } ) ConnectionMode.Con -> - SendContactRequestView( - disableSendButton = disableSendButton, - icon = MR.images.ic_person_add_filled, - sendRequest = { showSendConnectPreparedContactAlert() } - ) + if (chat.chatInfo.contact.isBot) { + ConnectButtonView( + text = stringResource(MR.strings.compose_view_connect), + icon = MR.images.ic_bolt_filled, + connect = { withApi { sendConnectPreparedContact() } } + ) + } else { + SendContactRequestView( + disableSendButton = disableSendButton, + icon = MR.images.ic_person_add_filled, + sendRequest = { showSendConnectPreparedContactAlert() } + ) + } } } else if ( chat.chatInfo is ChatInfo.Direct @@ -1480,7 +1523,7 @@ fun ComposeView( ) } else { Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { - AttachmentButton() + AttachmentAndCommandsButtons() SendMsgView_(disableSendButton = disableSendButton) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt index ac722783a3..03d0b99854 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt @@ -77,7 +77,7 @@ fun SelectedItemsButtonsToolbar( val forwardCountProhibited = remember { mutableStateOf(false) } Box { // It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty - ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) + ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(false) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) Row( Modifier .matchParentSize() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt index 62f1a4337c..81e7d80da3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt @@ -44,7 +44,7 @@ fun SelectedItemsMembersToolbar( ) { // It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty Box(Modifier.alpha(0f)) { - ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) + ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(false) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) } Row( Modifier diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 0f9b3151fe..6f873035f1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -65,7 +65,7 @@ data class ChatItemReactionMenuItem ( fun ChatItemView( chatsCtx: ChatModel.ChatsContext, rhId: Long?, - cInfo: ChatInfo, + chat: Chat, cItem: ChatItem, composeState: MutableState, imageProvider: (() -> ImageGalleryProvider)? = null, @@ -109,6 +109,7 @@ fun ChatItemView( itemSeparation: ItemSeparation, preview: Boolean = false, ) { + val cInfo = chat.chatInfo val uriHandler = LocalUriHandler.current val sent = cItem.chatDir.sent val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart @@ -327,7 +328,7 @@ fun ChatItemView( ) { @Composable fun framedItemView() { - FramedItemView(chatsCtx, cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToItemId, scrollToQuotedItemFromItem) + FramedItemView(chatsCtx, chat, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToItemId, scrollToQuotedItemFromItem) } fun deleteMessageQuestionText(): String { @@ -1449,7 +1450,7 @@ fun PreviewChatItemView( ChatItemView( chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), rhId = null, - ChatInfo.Direct.sampleData, + Chat.sampleData, chatItem, useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, @@ -1500,7 +1501,7 @@ fun PreviewChatItemViewDeletedContent() { ChatItemView( chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), rhId = null, - ChatInfo.Direct.sampleData, + Chat.sampleData, ChatItem.getDeletedContentSampleData(), useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index ad02b8d812..f36da6c908 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -21,14 +21,17 @@ import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.ComposeState import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlin.math.ceil @Composable fun FramedItemView( chatsCtx: ChatModel.ChatsContext, - chatInfo: ChatInfo, + chat: Chat, ci: ChatItem, uriHandler: UriHandler? = null, imageProvider: (() -> ImageGalleryProvider)? = null, @@ -43,6 +46,7 @@ fun FramedItemView( scrollToItemId: MutableState, scrollToQuotedItemFromItem: (Long) -> Unit = {}, ) { + val chatInfo = chat.chatInfo val sent = ci.chatDir.sent val chatTTL = chatInfo.timedMessagesTTL @@ -182,7 +186,7 @@ fun FramedItemView( fun ciFileView(ci: ChatItem, text: String) { CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, receiveFile) if (text != "" || ci.meta.isLive) { - CIMarkdownText(ci, chatInfo, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } @@ -295,7 +299,7 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCVideo -> { @@ -303,26 +307,26 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCVoice -> { CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) if (mc.text != "") { - CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCFile -> ciFileView(ci, mc.text) is MsgContent.MCUnknown -> if (ci.file == null) { - CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } else { ciFileView(ci, mc.text) } is MsgContent.MCLink -> { ChatItemLinkView(mc.preview, showMenu, onLongClick = { showMenu.value = true }) Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { - CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCReport -> { @@ -331,9 +335,9 @@ fun FramedItemView( append(if (mc.text.isEmpty()) mc.reason.text else "${mc.reason.text}: ") } } - CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix) } - else -> CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + else -> CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } @@ -353,8 +357,9 @@ fun FramedItemView( @Composable fun CIMarkdownText( + chatsCtx: ChatModel.ChatsContext, ci: ChatItem, - chatInfo: ChatInfo, + chat: Chat, chatTTL: Int?, linkMode: SimplexLinkMode, uriHandler: UriHandler?, @@ -364,9 +369,11 @@ fun CIMarkdownText( prefix: AnnotatedString? = null ) { Box(Modifier.padding(vertical = 7.dp, horizontal = 12.dp)) { + val chatInfo = chat.chatInfo val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text MarkdownText( text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true, + sendCommandMsg = if (chatInfo.useCommands && chat.chatInfo.sndReady) { { msg -> sendCommandMsg(chatsCtx, chat, msg) } } else null, meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode, mentions = ci.mentions, userMemberId = when { chatInfo is ChatInfo.Group -> chatInfo.groupInfo.membership.memberId @@ -377,6 +384,32 @@ fun CIMarkdownText( } } +fun sendCommandMsg(chatsCtx: ChatModel.ChatsContext, chat: Chat, msg: String) { + if (chat.chatInfo.sndReady) { + withLongRunningApi(slow = 60_000) { + val cInfo = chat.chatInfo + val chatItems = + chatModel.controller.apiSendMessages( + rh = chat.remoteHostId, + type = cInfo.chatType, + id = cInfo.apiId, + scope = cInfo.groupChatScope(), + composedMessages = listOf(ComposedMessage(fileSource = null, quotedItemId = null, msgContent = MsgContent.MCText(msg), mentions = emptyMap())) + ) + if (!chatItems.isNullOrEmpty()) { + chatItems.forEach { aChatItem -> + withContext(Dispatchers.Main) { + chatsCtx.addChatItem(chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem) + } + } + } + } + } else { + AlertManager.shared.showAlertMsg(MR.strings.cant_send_message_alert_title, MR.strings.cant_send_commands_alert_text) + } +} + + const val CHAT_IMAGE_LAYOUT_ID = "chatImage" const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble" const val CHAT_COMPOSE_LAYOUT_ID = "chatCompose" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index ad11eb4897..291eedb4ee 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.* import androidx.compose.ui.platform.* import androidx.compose.ui.text.* +import androidx.compose.ui.text.AnnotatedString.Range import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -61,6 +62,7 @@ fun MarkdownText ( mentions: Map? = null, userMemberId: String? = null, toggleSecrets: Boolean, + sendCommandMsg: ((String) -> Unit)? = null, style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp), maxLines: Int = Int.MAX_VALUE, overflow: TextOverflow = TextOverflow.Clip, @@ -134,7 +136,9 @@ fun MarkdownText ( } Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent?.second ?: mapOf()) } else { - var hasAnnotations = false + var hasLinks = false + var hasSecrets = false + var hasCommands = false val annotatedText = buildAnnotatedString { inlineContent?.first?.invoke(this) appendSender(this, sender, senderBold) @@ -143,7 +147,7 @@ fun MarkdownText ( if (ft.format == null) append(ft.text) else if (toggleSecrets && ft.format is Format.Secret) { val ftStyle = ft.format.style - hasAnnotations = true + hasSecrets = true val key = i.toString() withAnnotation(tag = "SECRET", annotation = key) { if (showSecrets[key] == true) append(ft.text) else withStyle(ftStyle) { append(ft.text) } @@ -168,10 +172,21 @@ fun MarkdownText ( } else { append(ft.text) } + } else if (ft.format is Format.Command) { + if (sendCommandMsg == null) { + append(ft.text) + } else { + hasCommands = true + val ftStyle = ft.format.style + val cmd = ft.format.commandStr + withAnnotation(tag = "COMMAND", annotation = cmd) { + withStyle(ftStyle) { append("/$cmd") } + } + } } else { val link = ft.link(linkMode) if (link != null) { - hasAnnotations = true + hasLinks = true val ftStyle = ft.format.style withAnnotation(tag = if (ft.format is Format.SimplexLink) "SIMPLEX_URL" else "URL", annotation = link) { withStyle(ftStyle) { append(ft.viewText(linkMode)) } @@ -189,51 +204,53 @@ fun MarkdownText ( withStyle(reserveTimestampStyle) { append("\n" + metaText) } else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) } } - if (hasAnnotations && uriHandler != null) { + if ((hasLinks && uriHandler != null) || hasSecrets || (hasCommands && sendCommandMsg != null)) { val icon = remember { mutableStateOf(PointerIcon.Default) } ClickableText(annotatedText, style = style, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow, onLongClick = { offset -> - annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) } - annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) } + if (hasLinks) { + annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) + .firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) } + annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) + .firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) } + } }, onClick = { offset -> - annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> + val withAnnotation: (String, (Range) -> Unit) -> Unit = { tag, f -> + annotatedText.getStringAnnotations(tag, start = offset, end = offset).firstOrNull()?.let(f) + } + if (hasLinks && uriHandler != null) { + withAnnotation("URL") { a -> try { - uriHandler.openUri(annotation.item) + uriHandler.openUri(a.item) } catch (e: Exception) { // It can happen, for example, when you click on a text 0.00001 but don't have any app that can catch // `tel:` scheme in url installed on a device (no phone app or contacts, maybe) Log.e(TAG, "Open url: ${e.stackTraceToString()}") } } - annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> - uriHandler.openVerifiedSimplexUri(annotation.item) - } - annotatedText.getStringAnnotations(tag = "SECRET", start = offset, end = offset) - .firstOrNull()?.let { annotation -> - val key = annotation.item + withAnnotation("SIMPLEX_URL") { a -> uriHandler.openVerifiedSimplexUri(a.item) } + } else if (hasSecrets) { + withAnnotation("SECRET") { a -> + val key = a.item showSecrets[key] = !(showSecrets[key] ?: false) } + } else if (hasCommands && sendCommandMsg != null) { + withAnnotation("COMMAND") { a -> sendCommandMsg("/${a.item}") } + } }, onHover = { offset -> - icon.value = annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) - .firstOrNull()?.let { + val hasAnnotation: (String) -> Boolean = { tag -> annotatedText.hasStringAnnotations(tag, start = offset, end = offset) } + icon.value = + if (hasAnnotation("URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) { PointerIcon.Hand - } ?: annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) - .firstOrNull()?.let { - PointerIcon.Hand - } ?: annotatedText.getStringAnnotations(tag = "SECRET", start = offset, end = offset) - .firstOrNull()?.let { - PointerIcon.Hand - } ?: PointerIcon.Default + } else { + PointerIcon.Default + } }, shouldConsumeEvent = { offset -> - annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset).any() - annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset).any() + annotatedText.hasStringAnnotations(tag = "URL", start = offset, end = offset) + || annotatedText.hasStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) } ) } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index e6c74b7558..35681ff1d2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -176,12 +176,14 @@ fun ChatPreviewView( is ChatInfo.Direct -> if (cInfo.contact.isContactCard) { stringResource(MR.strings.contact_tap_to_connect) to MaterialTheme.colors.primary + } else if (cInfo.contact.isBot && cInfo.contact.nextConnectPrepared) { + stringResource(MR.strings.open_to_use_bot) to Color.Unspecified } else if (cInfo.contact.sendMsgToConnect) { stringResource(MR.strings.open_to_connect) to Color.Unspecified } else if (cInfo.contact.nextAcceptContactRequest) { stringResource(MR.strings.open_to_accept) to Color.Unspecified } else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null && cInfo.contact.active) { - if (cInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con) { + if ((cInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con && !cInfo.contact.isBot) || cInfo.contact.contactGroupMemberId != null) { stringResource(MR.strings.contact_should_accept) to Color.Unspecified } else { stringResource(MR.strings.contact_connection_pending) to Color.Unspecified diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt index 75f61dda04..a55b1a0d56 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt @@ -2,14 +2,14 @@ package chat.simplex.common.views.helpers import androidx.compose.animation.core.* -fun chatListAnimationSpec() = tween(durationMillis = 250, easing = FastOutSlowInEasing) - -fun newChatSheetAnimSpec() = tween(256, 0, LinearEasing) +fun chatListAnimationSpec() = tween(durationMillis = 350, easing = FastOutSlowInEasing) fun audioProgressBarAnimationSpec() = tween(durationMillis = 30, easing = LinearEasing) -fun userPickerAnimSpec() = tween(256, 0, FastOutSlowInEasing) +fun userPickerAnimSpec() = tween(350, 0, FastOutSlowInEasing) -fun mentionPickerAnimSpec() = tween(256, 0, FastOutSlowInEasing) +fun mentionPickerAnimSpec() = tween(350, 0, FastOutSlowInEasing) -fun contextUserPickerAnimSpec() = tween(256, 0, FastOutSlowInEasing) +fun commandMenuAnimSpec() = tween(350, 0, FastOutSlowInEasing) + +fun contextUserPickerAnimSpec() = tween(350, 0, FastOutSlowInEasing) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt index 72fab4990b..72ef8c623d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt @@ -33,6 +33,7 @@ fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme when (chatInfo) { is ChatInfo.Group -> chatInfo.groupInfo.chatIconName is ChatInfo.Local -> MR.images.ic_folder_filled + is ChatInfo.Direct -> chatInfo.contact.chatIconName else -> MR.images.ic_account_circle_filled } ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 786e3483c5..b1c5caedc9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -390,7 +390,7 @@ private fun showOpenKnownContactAlert(chatModel: ChatModel, rhId: Long?, close: ProfileImage( size = alertProfileImageSize, image = contact.profile.image, - icon = MR.images.ic_account_circle_filled + icon = contact.chatIconName ) }, confirmText = generalGetString(if (contact.nextConnectPrepared) MR.strings.connect_plan_open_new_chat else MR.strings.connect_plan_open_chat), @@ -474,7 +474,7 @@ private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: (( ProfileImage( size = alertProfileImageSize, image = groupInfo.groupProfile.image, - icon = if (groupInfo.businessChat == null) MR.images.ic_supervised_user_circle_filled else MR.images.ic_work_filled_padded + icon = groupInfo.chatIconName ) }, confirmText = generalGetString( @@ -515,7 +515,10 @@ fun showPrepareContactAlert( ProfileImage( size = alertProfileImageSize, image = contactShortLinkData.profile.image, - icon = if (contactShortLinkData.business) MR.images.ic_work_filled_padded else MR.images.ic_account_circle_filled + icon = + if (contactShortLinkData.business) MR.images.ic_work_filled_padded + else if (contactShortLinkData.profile.peerType == ChatPeerType.Bot) MR.images.ic_cube + else MR.images.ic_account_circle_filled ) }, confirmText = generalGetString(MR.strings.connect_plan_open_new_chat), diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index e962c3a646..22cdd3a643 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -449,6 +449,7 @@ No chats found Tap to Connect Open to connect + Open to use bot Open to accept contact should accept… Connect with %1$s? @@ -489,8 +490,10 @@ Tap Connect to chat Tap Connect to send request + Tap Connect to use bot Accept contact request Your contact + Bot Tap Join group Your group Group @@ -562,6 +565,7 @@ you are observer reviewed by admins member has old version + To send commands you must be connected. Image @@ -2157,6 +2161,9 @@ Allow your contacts to send voice messages. Allow voice messages only if your contact allows them. Prohibit sending voice messages. + Allow your contacts to send files and media. + Allow files and media only if your contact allows them. + Prohibit sending files and media. Allow your contacts adding message reactions. Allow message reactions only if your contact allows them. Prohibit message reactions. @@ -2175,6 +2182,10 @@ Only you can send voice messages. Only your contact can send voice messages. Voice messages are prohibited in this chat. + Both you and your contact can send files and media. + Only you can send files and media. + Only your contact can send files and media. + Files and media are prohibited in this chat. Both you and your contact can add message reactions. Only you can add message reactions. Only your contact can add message reactions. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cube.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cube.svg new file mode 100644 index 0000000000..252afc1acc --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs index 8107b664c4..d9f091a13a 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -11,10 +11,11 @@ import Data.Text (Text) import Options.Applicative import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (updateStr, versionNumber, versionString) -import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, coreChatOptsP) +import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, CreateBotOpts (..), coreChatOptsP) data BroadcastBotOpts = BroadcastBotOpts { coreOptions :: CoreChatOpts, + botDisplayName :: Text, publishers :: [KnownContact], welcomeMessage :: Text, prohibitedMessage :: Text @@ -29,6 +30,12 @@ defaultProhibitedMessage ps = "Sorry, only these users can broadcast messages: " broadcastBotOpts :: FilePath -> FilePath -> Parser BroadcastBotOpts broadcastBotOpts appDir defaultDbName = do coreOptions <- coreChatOptsP appDir defaultDbName + botDisplayName <- + strOption + ( long "display-name" + <> metavar "DISPLAY_NAME" + <> help "The display name of the broadcast bot" + ) publishers <- option parseKnownContacts @@ -55,6 +62,7 @@ broadcastBotOpts appDir defaultDbName = do pure BroadcastBotOpts { coreOptions, + botDisplayName, publishers, welcomeMessage = fromMaybe (defaultWelcomeMessage publishers) welcomeMessage_, prohibitedMessage = fromMaybe (defaultProhibitedMessage publishers) prohibitedMessage_ @@ -72,7 +80,7 @@ getBroadcastBotOpts appDir defaultDbName = versionAndUpdate = versionStr <> "\n" <> updateStr mkChatOpts :: BroadcastBotOpts -> ChatOpts -mkChatOpts BroadcastBotOpts {coreOptions} = +mkChatOpts BroadcastBotOpts {coreOptions, botDisplayName} = ChatOpts { coreOptions, chatCmd = "", @@ -86,5 +94,6 @@ mkChatOpts BroadcastBotOpts {coreOptions} = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, + createBot = Just CreateBotOpts {botDisplayName, allowFiles = False}, maintenance = False } diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 2c26905e79..2eba633609 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -16,7 +16,7 @@ import qualified Data.Text as T import Options.Applicative import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (updateStr, versionNumber, versionString) -import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, coreChatOptsP) +import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, CreateBotOpts (..), coreChatOptsP) data DirectoryOpts = DirectoryOpts { coreOptions :: CoreChatOpts, @@ -119,7 +119,7 @@ directoryOpts appDir defaultDbName = do <> help "The display name of the directory service bot, without *'s and spaces (SimpleX-Directory)" <> value "SimpleX-Directory" ) - runCLI <- + runCLI <- switch ( long "run-cli" <> help "Run directory service as CLI" @@ -155,7 +155,7 @@ getDirectoryOpts appDir defaultDbName = versionAndUpdate = versionStr <> "\n" <> updateStr mkChatOpts :: DirectoryOpts -> ChatOpts -mkChatOpts DirectoryOpts {coreOptions} = +mkChatOpts DirectoryOpts {coreOptions, serviceName} = ChatOpts { coreOptions, chatCmd = "", @@ -169,5 +169,6 @@ mkChatOpts DirectoryOpts {coreOptions} = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, + createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False}, maintenance = False } diff --git a/bots/README.md b/bots/README.md index 0b1943b7a7..9449e9d847 100644 --- a/bots/README.md +++ b/bots/README.md @@ -1,6 +1,8 @@ # SimpleX Chat bot API - [Why create a bot](#why-create-a-bot) +- [What is SimpleX bot](#what-is-simplex-bot) +- [How to configure bot profile](#how-to-configure-bot-profile) - [How to create a bot](#how-to-create-a-bot) - [Sending commands](#sending-commands) - [Processing events](#processing-events) @@ -12,8 +14,9 @@ ## Why create a bot You can implement SimpleX Chat for these and many other scenarios: -- customer support - both as a single- and a multi-agent support chat (using SimpleX Chat [business address]() feature), +- customer support - both as a single- and a multi-agent support chat (using SimpleX Chat [business address](https://simplex.chat/docs/business.html) feature), - information search and retrieval bots, with or without LLM integration, +- moderation bots, to moderate your group and communities. - broadcast bot, when messages from your trusted users are forwarded to all connected contacts - e.g., see our SimpleX Status bot in the app ([source code](../apps/simplex-broadcast-bot/)), - feedback bot, when messages from connected contacts are forwarded to a preset list of your trusted users, - P2P trading bots, connecting buyers and sellers, @@ -22,6 +25,65 @@ You can implement SimpleX Chat for these and many other scenarios: We will share all useful bots you create in the bottom of this page - please submit a PR to add it. +## What is SimpleX bot + +SimpleX bot is a participant of SimpleX network. Theoretically, bot can do everything that a usual SimpleX Chat user can do – send and receive messages and files, connect to addresses and join groups, etc. But to be useful, a bot should distinguish itself as a bot, and to provide an interface for the users to interact with it. + +## How to configure bot profile + +Starting from v6.4.3, SimpleX Chat apps support bot configuration to distinguish bots, to highlight commands in messages, and to show command menus. + +### Set up bot profile + +To distinguish SimpleX user profile as a bot, set its `peerType` property to `"bot"`. It can be done in one of these ways: +- using CLI options `--create-bot-display-name` and `create-bot-allow-files` when first starting CLI to create bot profile, +- using command `/create bot [files=on] [ ]` (if name contains spaces, it must be in single quotes), when creating additional bot profiles in the same database, +- by configuring bot commands that the users will see in the UI when they type `/` character or tap `//` button with `/set bot commands ...` CLI command (see syntax below), +- by using [APIUpdateProfile](./api/COMMANDS.md#apiupdateprofile) bot command to set `peerType` and configure bot commands at the same time. + +### Configure bot commands + +Bot commands are messages that start from `/` character. Normally, they would consist of lowercase latin letters, but commands can use any letters, digits and underscores. Commands can have parameters. + +All commands in messages will be highlighted in the chats with the bot, and when users tap them, they will be instantly sent. If the message has a single line and starts from `/` character, the whole message will be highlighted. Otherwise, if command is included as part of the message, it will be highlighted until the first space after `/` character: e.g., `/list` command in Directory service shows user's groups. + +*Please note*: commands in messages will be highlighted based purely on `/` character, regardless of whether they are supported by the bot or included in bot configuration. It allows bots to have "hidden" commands that bot would support, but that won't be shown in the menu. But it may also lead to mistakes if bot sends incorrect commands in the instructions to the users. + +Bots can also send highlighted commands with parameters. To do that, bots should surround both command and its parameters in single quotes: e.g., `/'role 2'`. Quotes won't show in the apps UI, and if the user taps this command, it will be sent as `/role 2`. + +Configured bot commands will be be offered to the users as a menu, and for quick lookup as the user types. + +Bot commands configuration is a property in `preferences` object in bot profile received by the user. These preferences can be configured both on the bot user profile level, to offer the same commands to all connected users, and as overrides for specific contacts, to offer different commands to different bot contacts. + +Configuring commands in bot user can be done either with [APIUpdateProfile](./api/COMMANDS.md#apiupdateprofile) or with `/set bot commands` CLI command: + +``` +/set bot commands +``` + +where: + +``` +commands = [,...] +commandOrMenu = command | menu +command = ' - + Error creating address + خطأ في إنشاء العنوان No comment provided by engineer. - + Error creating group + خطأ في إنشاء المجموعة No comment provided by engineer. @@ -5757,6 +5759,102 @@ This is your own one-time link! Encryption re-negotiation failed. فشل إعادة التفاوض على التشفير. + + Accept as member + اقبل كعضو + + + Accept as observer + اقبل كمراقب + + + Accept contact request + اقبل طلب الاتصال + + + Accept member + اقبل العضو + + + Add message + أضف رسالة + + + All servers + كل الخوادم + + + Bio + نبذة + + + Bio too large + النبذة كبيرة جدًا + + + Can't change profile + لا يمكن تغيير الحساب + + + Chat with admins + تحدث مع المدراء + + + Chat with member + تحدث مع العضو + + + Chat with members before they join. + تحدث مع الأعضاء قبل انضمامهم. + + + Chats with members + تحدث مع الأعضاء + + + Connect faster! 🚀 + اتصل بسرعة! 🚀 + + + Contact requests from groups + طلبات الاتصال من المجموعات + + + Create your address + أنشئ عنوانك + + + Delete chat with member? + حذف المحادثة مع العضو؟ + + + Description too large + الوصف كبير جدًا + + + Empty message! + رسالة فارغة! + + + Enable disappearing messages by default. + فعّل حذف الرسائل تلقائيا. + + + Error accepting member + خطأ في قبول العضو + + + Error adding short link + خطأ في إضافة الرابط القصير + + + Error changing chat profile + خطأ في تغيير حساب المحادثات + + + Error connecting to forwarding server %@. Please try later. + خطأ في الإتصال بخادم التحويل @%. حاول مجددا بعد حين. + diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 3a9c362405..f42e5d20ce 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -1652,7 +1652,7 @@ set passcode view Chat with members before they join. - Chat mit Mitgliedern bevor sie beitreten. + Mit Mitgliedern chatten bevor sie beitreten. No comment provided by engineer. @@ -2103,6 +2103,7 @@ Das ist Ihr eigener Einmal-Link! Contact requests from groups + KONTAKTANFRAGEN VON GRUPPEN No comment provided by engineer. @@ -3037,7 +3038,7 @@ chat item action Enable disappearing messages by default. - Verschwindende Nachrichten per Voreinstellung aktiviert. + Verschwindende Nachrichten sind per Voreinstellung aktiviert. No comment provided by engineer. @@ -3547,6 +3548,7 @@ chat item action Error setting auto-accept + Fehler bei der Einstellung des automatischen Akzeptierens No comment provided by engineer. @@ -4682,7 +4684,7 @@ Das ist Ihr Link für die Gruppe %@! Keep your chats clean - Halten Sie Ihre Chats übersichtlich + Ihre Chats übersichtlich halten No comment provided by engineer. @@ -4892,6 +4894,7 @@ Das ist Ihr Link für die Gruppe %@! Member is deleted - can't accept request + Mitglied ist gelöscht - Anfrage kann nicht angenommen werden No comment provided by engineer. @@ -7409,7 +7412,7 @@ chat item action Set profile bio and welcome message. - Geben Sie eine Profil-Biografie und eine Begrüßungsmeldung ein. + Sie können eine Profil-Biografie und eine Begrüßungsmeldung eingeben. No comment provided by engineer. @@ -8274,6 +8277,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro This setting is for your current profile **%@**. + Diese Einstellung gilt für Ihr aktuelles Profil **%@**. No comment provided by engineer. @@ -10355,10 +10359,12 @@ time to disappear requested connection + Angefragte Verbindung rcv group event chat item requested connection from group %@ + Angefragte Verbindung von Gruppe %@ rcv direct event chat item diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 1454ad639d..7f33515577 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -859,7 +859,7 @@ swipe action Allow disappearing messages only if your contact allows it to you. - Se permiten los mensajes temporales pero sólo si tu contacto también los permite para tí. + Se permiten los mensajes temporales pero sólo si tu contacto también los permite. No comment provided by engineer. @@ -873,7 +873,7 @@ swipe action Allow irreversible message deletion only if your contact allows it to you. (24 hours) - Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también la permite para tí. (24 horas) + Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite. (24 horas) No comment provided by engineer. @@ -1642,7 +1642,7 @@ set passcode view Chat with admins - Chatea con los administradores + Chatea con administradores chat toolbar @@ -1692,7 +1692,7 @@ set passcode view Choose file - Elije archivo + Elegir archivo No comment provided by engineer. @@ -2103,6 +2103,7 @@ This is your own one-time link! Contact requests from groups + Solicitudes de contacto en grupo No comment provided by engineer. @@ -3547,6 +3548,7 @@ chat item action Error setting auto-accept + Error al configurar auto aceptar No comment provided by engineer. @@ -4625,17 +4627,17 @@ More improvements are coming soon! Join - Unirte + Unirme swipe action Join as %@ - unirte como %@ + Unirme como %@ No comment provided by engineer. Join group - Unirte al grupo + Unirme al grupo new chat sheet title @@ -4645,7 +4647,7 @@ More improvements are coming soon! Join incognito - Unirte en modo incógnito + Unirme en modo incógnito No comment provided by engineer. @@ -4892,6 +4894,7 @@ This is your link for group %@! Member is deleted - can't accept request + Miembro eliminado, no puede aceptar solicitudes No comment provided by engineer. @@ -4901,7 +4904,7 @@ This is your link for group %@! Member role will be changed to "%@". All chat members will be notified. - El rol del miembro cambiará a "%@" y todos serán notificados. + El rol del miembro cambiará a "%@". Se notificará en el chat. No comment provided by engineer. @@ -5091,7 +5094,7 @@ This is your link for group %@! Messages from %@ will be shown! - ¡Los mensajes de %@ serán mostrados! + ¡Los mensajes nuevos de %@ serán mostrados! No comment provided by engineer. @@ -5411,7 +5414,7 @@ This is your link for group %@! No chats with members - Sin chats con miembros + Sin chats No comment provided by engineer. @@ -5809,7 +5812,7 @@ Requiere activación de la VPN. Open to join - Abrir para unirte + Abre para unirte No comment provided by engineer. @@ -7630,7 +7633,7 @@ chat item action SimpleX address or 1-time link? - ¿Dirección SimpleX o enlace de un uso? + ¿Dirección SimpleX o enlace de un solo uso? No comment provided by engineer. @@ -7957,7 +7960,7 @@ report reason Tap Join group - Pulsa Unirte al grupo + Pulsa Unirme al grupo No comment provided by engineer. @@ -7987,7 +7990,7 @@ report reason Tap to paste link - Pulsa para pegar el enlacePulsa para pegar enlace + Pulsa aquí para pegar el enlace No comment provided by engineer. @@ -8239,7 +8242,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. This display name is invalid. Please choose another name. - Éste nombre mostrado no es válido. Por favor, elije otro nombre. + Éste nombre mostrado no es válido. Por favor, elige otro nombre. No comment provided by engineer. @@ -8274,6 +8277,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. This setting is for your current profile **%@**. + Esta configuración se aplica al perfil actual **%@**. No comment provided by engineer. @@ -8469,7 +8473,7 @@ Se te pedirá que completes la autenticación antes de activar esta función. Unblock member for all? - ¿Desbloquear el miembro para todos? + ¿Desbloquear al miembro para todos? No comment provided by engineer. @@ -9258,7 +9262,7 @@ Repeat join request? You can view your reports in Chat with admins. - Puedes ver tus informes en Chat con los administradores. + Puedes ver tus informes en Chat con administradores. alert message @@ -9380,12 +9384,12 @@ Repeat connection request? You will stop receiving messages from this chat. Chat history will be preserved. - Dejarás de recibir mensajes de este chat. El historial del chat se conserva. + Dejarás de recibir mensajes del chat. El historial del chat se conserva. No comment provided by engineer. You will stop receiving messages from this group. Chat history will be preserved. - Dejarás de recibir mensajes de este grupo. El historial del chat se conservará. + Dejarás de recibir mensajes del grupo. El historial del chat se conservará. No comment provided by engineer. @@ -9836,12 +9840,12 @@ marked deleted chat item preview text contact not ready - el contacto no está listo + en espera de ser aceptado No comment provided by engineer. contact should accept… - el contacto debe aceptar… + el contacto debe aceptarte… No comment provided by engineer. @@ -10355,10 +10359,12 @@ time to disappear requested connection + conexión solicitada rcv group event chat item requested connection from group %@ + conexión solicitada desde el grupo %@ rcv direct event chat item diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 156d12ea9f..af02421c01 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -2103,6 +2103,7 @@ Ez a saját egyszer használható meghívója! Contact requests from groups + Partneri kapcsolatkérések a csoportokból No comment provided by engineer. @@ -3547,6 +3548,7 @@ chat item action Error setting auto-accept + Hiba az automatikus elfogadás beállításakor No comment provided by engineer. @@ -4892,6 +4894,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Member is deleted - can't accept request + A tag törölve lett – nem lehet elfogadni a kérést No comment provided by engineer. @@ -8274,6 +8277,7 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. This setting is for your current profile **%@**. + Ez a beállítás csak a jelenlegi **%@** nevű csevegési profiljára vonatkozik. No comment provided by engineer. @@ -10177,7 +10181,7 @@ pref value moderated by %@ - moderálva lett %@ által + %@ moderálta ezt az üzenetet marked deleted chat item preview text @@ -10355,10 +10359,12 @@ time to disappear requested connection + partneri kapcsolatot kért rcv group event chat item requested connection from group %@ + a(z) %@ nevű csoportból partneri kapcsolatot kért rcv direct event chat item diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 5d3e962d25..a54056dc4d 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -2103,6 +2103,7 @@ Questo è il tuo link una tantum! Contact requests from groups + Richieste di contatto dai gruppi No comment provided by engineer. @@ -3547,6 +3548,7 @@ chat item action Error setting auto-accept + Errore impostando l'accettazione automatica No comment provided by engineer. @@ -4892,6 +4894,7 @@ Questo è il tuo link per il gruppo %@! Member is deleted - can't accept request + Il membro è eliminato - impossibile accettare la richiesta No comment provided by engineer. @@ -8274,6 +8277,7 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa. This setting is for your current profile **%@**. + Questa impostazione è per il tuo profilo attuale **%@**. No comment provided by engineer. @@ -10355,10 +10359,12 @@ time to disappear requested connection + connessione richiesta rcv group event chat item requested connection from group %@ + connessione richiesta dal gruppo %@ rcv direct event chat item diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index b76edfc106..44db30b201 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -192,6 +192,7 @@ %d seconds(s) + %d 秒 delete after time @@ -206,7 +207,6 @@ %lld - No comment provided by engineer. @@ -465,6 +465,7 @@ time interval 1 year + 1年 delete after time @@ -548,6 +549,7 @@ time interval About operators + オペレーターについて No comment provided by engineer. @@ -564,14 +566,17 @@ swipe action Accept as member + メンバーとして承認する alert action Accept as observer + オブザーバーとして承認する alert action Accept conditions + 条件に同意する No comment provided by engineer. @@ -581,6 +586,7 @@ swipe action Accept contact request + 連絡先リクエストを受け入れる alert title @@ -596,10 +602,12 @@ swipe action Accept member + メンバーを承認する alert title Accepted conditions + 承諾された条件 No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff index eeed89c00e..01b8f7e244 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -39,7 +39,7 @@ Available in v5.1 (can be copied) - .(pode ser copiado) + (pode ser copiado) No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 5f26c285f5..3e94c4acd2 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -2103,6 +2103,7 @@ Bu senin kendi tek kullanımlık bağlantın! Contact requests from groups + Gruplardan gelen iletişim talepleri No comment provided by engineer. @@ -2222,6 +2223,7 @@ Bu senin kendi tek kullanımlık bağlantın! Create your address + Adresinizi oluşturun No comment provided by engineer. @@ -3036,6 +3038,7 @@ chat item action Enable disappearing messages by default. + Varsayılan olarak kaybolan mesajları etkinleştirin. No comment provided by engineer. diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index a5fa71198b..5b5646c470 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -4301,7 +4301,7 @@ chat item action */ /* alert message */ "You can view invitation link again in connection details." = "Можете да видите отново линкът за покана в подробностите за връзката."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Не може да изпращате съобщения!"; /* chat item text */ diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index cd90568881..47bfd2aa12 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -3363,7 +3363,7 @@ chat item action */ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "K formátování zpráv můžete použít markdown:"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Nemůžete posílat zprávy!"; /* chat item text */ diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 350729e01c..f00d0a7378 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1087,7 +1087,7 @@ set passcode view */ "Chat with member" = "Chat mit einem Mitglied"; /* No comment provided by engineer. */ -"Chat with members before they join." = "Chat mit Mitgliedern bevor sie beitreten."; +"Chat with members before they join." = "Mit Mitgliedern chatten bevor sie beitreten."; /* No comment provided by engineer. */ "Chats" = "Chats"; @@ -1410,6 +1410,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact preferences" = "Kontakt-Präferenzen"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "KONTAKTANFRAGEN VON GRUPPEN"; + /* No comment provided by engineer. */ "contact should accept…" = "Kontakt sollte akzeptieren…"; @@ -2011,7 +2014,7 @@ chat item action */ "Enable camera access" = "Kamera-Zugriff aktivieren"; /* No comment provided by engineer. */ -"Enable disappearing messages by default." = "Verschwindende Nachrichten per Voreinstellung aktiviert."; +"Enable disappearing messages by default." = "Verschwindende Nachrichten sind per Voreinstellung aktiviert."; /* No comment provided by engineer. */ "Enable Flux in Network & servers settings for better metadata privacy." = "Für einen besseren Metadatenschutz Flux in den Netzwerk- und Servereinstellungen aktivieren."; @@ -2367,6 +2370,9 @@ chat item action */ /* No comment provided by engineer. */ "Error sending message" = "Fehler beim Senden der Nachricht"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Fehler bei der Einstellung des automatischen Akzeptierens"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Fehler beim Setzen von Empfangsbestätigungen!"; @@ -3105,7 +3111,7 @@ snd error text */ "Keep unused invitation?" = "Nicht genutzte Einladung behalten?"; /* No comment provided by engineer. */ -"Keep your chats clean" = "Halten Sie Ihre Chats übersichtlich"; +"Keep your chats clean" = "Ihre Chats übersichtlich halten"; /* No comment provided by engineer. */ "Keep your connections" = "Ihre Verbindungen beibehalten"; @@ -3248,6 +3254,9 @@ snd error text */ /* item status text */ "Member inactive" = "Mitglied inaktiv"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "Mitglied ist gelöscht - Anfrage kann nicht angenommen werden"; + /* chat feature */ "Member reports" = "Mitglieder-Meldungen"; @@ -4422,6 +4431,12 @@ swipe action */ /* No comment provided by engineer. */ "request to join rejected" = "Beitrittsanfrage abgelehnt"; +/* rcv group event chat item */ +"requested connection" = "Angefragte Verbindung"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "Angefragte Verbindung von Gruppe %@"; + /* chat list item title */ "requested to connect" = "Zur Verbindung aufgefordert"; @@ -4898,7 +4913,7 @@ chat item action */ "Set passphrase to export" = "Passwort für den Export festlegen"; /* No comment provided by engineer. */ -"Set profile bio and welcome message." = "Geben Sie eine Profil-Biografie und eine Begrüßungsmeldung ein."; +"Set profile bio and welcome message." = "Sie können eine Profil-Biografie und eine Begrüßungsmeldung eingeben."; /* No comment provided by engineer. */ "Set the message shown to new members!" = "Definieren Sie eine Begrüßungsmeldung, die neuen Mitgliedern angezeigt wird!"; @@ -5430,6 +5445,9 @@ report reason */ /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil **%@**."; +/* No comment provided by engineer. */ +"This setting is for your current profile **%@**." = "Diese Einstellung gilt für Ihr aktuelles Profil **%@**."; + /* No comment provided by engineer. */ "Time to disappear is set only for new contacts." = "Die Zeit bis zum Verschwinden wird nur für neue Kontakte eingestellt."; @@ -6090,7 +6108,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "Sie können Ihre Meldungen im Chat mit den Administratoren sehen."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Sie können keine Nachrichten versenden!"; /* chat item text */ diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 80c3d7cc93..b9fda78da4 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -552,13 +552,13 @@ swipe action */ "Allow calls?" = "¿Permitir llamadas?"; /* No comment provided by engineer. */ -"Allow disappearing messages only if your contact allows it to you." = "Se permiten los mensajes temporales pero sólo si tu contacto también los permite para tí."; +"Allow disappearing messages only if your contact allows it to you." = "Se permiten los mensajes temporales pero sólo si tu contacto también los permite."; /* No comment provided by engineer. */ "Allow downgrade" = "Permitir versión anterior"; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también la permite para tí. (24 horas)"; +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite. (24 horas)"; /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "Se permiten las reacciones a los mensajes pero sólo si tu contacto también las permite."; @@ -1081,7 +1081,7 @@ set passcode view */ "Chat will be deleted for you - this cannot be undone!" = "El chat será eliminado para tí. ¡No puede deshacerse!"; /* chat toolbar */ -"Chat with admins" = "Chatea con los administradores"; +"Chat with admins" = "Chatea con administradores"; /* No comment provided by engineer. */ "Chat with member" = "Chat con miembro"; @@ -1111,7 +1111,7 @@ set passcode view */ "Choose _Migrate from another device_ on the new device and scan QR code." = "En el nuevo dispositivo selecciona _Migrar desde otro dispositivo_ y escanéa el código QR."; /* No comment provided by engineer. */ -"Choose file" = "Elije archivo"; +"Choose file" = "Elegir archivo"; /* No comment provided by engineer. */ "Choose from library" = "Elige de la biblioteca"; @@ -1405,13 +1405,16 @@ set passcode view */ "Contact name" = "Contacto"; /* No comment provided by engineer. */ -"contact not ready" = "el contacto no está listo"; +"contact not ready" = "en espera de ser aceptado"; /* No comment provided by engineer. */ "Contact preferences" = "Preferencias de contacto"; /* No comment provided by engineer. */ -"contact should accept…" = "el contacto debe aceptar…"; +"Contact requests from groups" = "Solicitudes de contacto en grupo"; + +/* No comment provided by engineer. */ +"contact should accept…" = "el contacto debe aceptarte…"; /* No comment provided by engineer. */ "Contact will be deleted - this cannot be undone!" = "El contacto será eliminado. ¡No puede deshacerse!"; @@ -2367,6 +2370,9 @@ chat item action */ /* No comment provided by engineer. */ "Error sending message" = "Error al enviar mensaje"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Error al configurar auto aceptar"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "¡Error al configurar confirmaciones de entrega!"; @@ -3072,19 +3078,19 @@ snd error text */ "Japanese interface" = "Interfáz en japonés"; /* swipe action */ -"Join" = "Unirte"; +"Join" = "Unirme"; /* No comment provided by engineer. */ -"Join as %@" = "unirte como %@"; +"Join as %@" = "Unirme como %@"; /* new chat sheet title */ -"Join group" = "Unirte al grupo"; +"Join group" = "Unirme al grupo"; /* No comment provided by engineer. */ "Join group conversations" = "Unirse a la conversación del grupo"; /* No comment provided by engineer. */ -"Join incognito" = "Unirte en modo incógnito"; +"Join incognito" = "Unirme en modo incógnito"; /* new chat action */ "Join your group?\nThis is your link for group %@!" = "¿Unirse a tu grupo?\n¡Este es tu enlace para el grupo %@!"; @@ -3248,11 +3254,14 @@ snd error text */ /* item status text */ "Member inactive" = "Miembro inactivo"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "Miembro eliminado, no puede aceptar solicitudes"; + /* chat feature */ "Member reports" = "Informes de miembros"; /* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "El rol del miembro cambiará a \"%@\" y todos serán notificados."; +"Member role will be changed to \"%@\". All chat members will be notified." = "El rol del miembro cambiará a \"%@\". Se notificará en el chat."; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "El rol del miembro cambiará a \"%@\" y se notificará al grupo."; @@ -3372,7 +3381,7 @@ snd error text */ "Messages are protected by **end-to-end encryption**." = "Los mensajes están protegidos mediante **cifrado de extremo a extremo**."; /* No comment provided by engineer. */ -"Messages from %@ will be shown!" = "¡Los mensajes de %@ serán mostrados!"; +"Messages from %@ will be shown!" = "¡Los mensajes nuevos de %@ serán mostrados!"; /* alert message */ "Messages in this chat will never be deleted." = "Los mensajes de esta conversación nunca se eliminan."; @@ -3591,7 +3600,7 @@ snd error text */ "No chats in list %@" = "Sin chats en la lista %@"; /* No comment provided by engineer. */ -"No chats with members" = "Sin chats con miembros"; +"No chats with members" = "Sin chats"; /* No comment provided by engineer. */ "No contacts selected" = "Ningún contacto seleccionado"; @@ -3848,7 +3857,7 @@ new chat action */ "Open to connect" = "Abrir para conectar"; /* No comment provided by engineer. */ -"Open to join" = "Abrir para unirte"; +"Open to join" = "Abre para unirte"; /* No comment provided by engineer. */ "Opening app…" = "Iniciando aplicación…"; @@ -4422,6 +4431,12 @@ swipe action */ /* No comment provided by engineer. */ "request to join rejected" = "petición para unirse rechazada"; +/* rcv group event chat item */ +"requested connection" = "conexión solicitada"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "conexión solicitada desde el grupo %@"; + /* chat list item title */ "requested to connect" = "solicitado para conectar"; @@ -5013,7 +5028,7 @@ chat item action */ "SimpleX address and 1-time links are safe to share via any messenger." = "Compartir enlaces de un solo uso y direcciones SimpleX es seguro a través de cualquier medio."; /* No comment provided by engineer. */ -"SimpleX address or 1-time link?" = "¿Dirección SimpleX o enlace de un uso?"; +"SimpleX address or 1-time link?" = "¿Dirección SimpleX o enlace de un solo uso?"; /* alert title */ "SimpleX address settings" = "Auto aceptar configuración"; @@ -5221,7 +5236,7 @@ report reason */ "Tap Create SimpleX address in the menu to create it later." = "Pulsa Crear dirección SimpleX en el menú para crearla más tarde."; /* No comment provided by engineer. */ -"Tap Join group" = "Pulsa Unirte al grupo"; +"Tap Join group" = "Pulsa Unirme al grupo"; /* No comment provided by engineer. */ "Tap to activate profile." = "Pulsa sobre un perfil para activarlo."; @@ -5236,7 +5251,7 @@ report reason */ "Tap to join incognito" = "Pulsa para unirte en modo incógnito"; /* No comment provided by engineer. */ -"Tap to paste link" = "Pulsa para pegar el enlacePulsa para pegar enlace"; +"Tap to paste link" = "Pulsa aquí para pegar el enlace"; /* No comment provided by engineer. */ "Tap to scan" = "Pulsa para escanear"; @@ -5410,7 +5425,7 @@ report reason */ "This device name" = "Nombre del dispositivo"; /* No comment provided by engineer. */ -"This display name is invalid. Please choose another name." = "Éste nombre mostrado no es válido. Por favor, elije otro nombre."; +"This display name is invalid. Please choose another name." = "Éste nombre mostrado no es válido. Por favor, elige otro nombre."; /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "Este grupo tiene más de %lld miembros, no se enviarán confirmaciones de entrega."; @@ -5430,6 +5445,9 @@ report reason */ /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Esta configuración se aplica a los mensajes del perfil actual **%@**."; +/* No comment provided by engineer. */ +"This setting is for your current profile **%@**." = "Esta configuración se aplica al perfil actual **%@**."; + /* No comment provided by engineer. */ "Time to disappear is set only for new contacts." = "Mensajes temporales activados sólo para los contactos nuevos."; @@ -5542,7 +5560,7 @@ report reason */ "Unblock member" = "Desbloquear miembro"; /* No comment provided by engineer. */ -"Unblock member for all?" = "¿Desbloquear el miembro para todos?"; +"Unblock member for all?" = "¿Desbloquear al miembro para todos?"; /* No comment provided by engineer. */ "Unblock member?" = "¿Desbloquear miembro?"; @@ -6088,9 +6106,9 @@ report reason */ "You can view invitation link again in connection details." = "Puedes ver el enlace de invitación de nuevo en los detalles de la conexión."; /* alert message */ -"You can view your reports in Chat with admins." = "Puedes ver tus informes en Chat con los administradores."; +"You can view your reports in Chat with admins." = "Puedes ver tus informes en Chat con administradores."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "¡No puedes enviar mensajes!"; /* chat item text */ @@ -6187,10 +6205,10 @@ report reason */ "You will still receive calls and notifications from muted profiles when they are active." = "Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos."; /* No comment provided by engineer. */ -"You will stop receiving messages from this chat. Chat history will be preserved." = "Dejarás de recibir mensajes de este chat. El historial del chat se conserva."; +"You will stop receiving messages from this chat. Chat history will be preserved." = "Dejarás de recibir mensajes del chat. El historial del chat se conserva."; /* No comment provided by engineer. */ -"You will stop receiving messages from this group. Chat history will be preserved." = "Dejarás de recibir mensajes de este grupo. El historial del chat se conservará."; +"You will stop receiving messages from this group. Chat history will be preserved." = "Dejarás de recibir mensajes del grupo. El historial del chat se conservará."; /* No comment provided by engineer. */ "You won't lose your contacts if you later delete your address." = "Si lo eliminas más tarde tus contactos no se perderán."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index b1a21bfc69..670607daa9 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -3258,7 +3258,7 @@ chat item action */ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Voit käyttää markdownia viestien muotoiluun:"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Et voi lähettää viestejä!"; /* chat item text */ diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 4502e4a292..be6af95358 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -5540,7 +5540,7 @@ chat item action */ /* alert message */ "You can view invitation link again in connection details." = "Vous pouvez à nouveau consulter le lien d'invitation dans les détails de la connexion."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Vous ne pouvez pas envoyer de messages !"; /* chat item text */ diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 6b902b5850..a09637a77d 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -1410,6 +1410,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact preferences" = "Partnerbeállítások"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "Partneri kapcsolatkérések a csoportokból"; + /* No comment provided by engineer. */ "contact should accept…" = "a partnernek el kell fogadnia…"; @@ -2367,6 +2370,9 @@ chat item action */ /* No comment provided by engineer. */ "Error sending message" = "Hiba történt az üzenet elküldésekor"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Hiba az automatikus elfogadás beállításakor"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Hiba történt a kézbesítési jelentések beállításakor!"; @@ -3248,6 +3254,9 @@ snd error text */ /* item status text */ "Member inactive" = "Inaktív tag"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "A tag törölve lett – nem lehet elfogadni a kérést"; + /* chat feature */ "Member reports" = "Tagok jelentései"; @@ -3447,7 +3456,7 @@ snd error text */ "Moderated at: %@" = "Moderálva: %@"; /* marked deleted chat item preview text */ -"moderated by %@" = "moderálva lett %@ által"; +"moderated by %@" = "%@ moderálta ezt az üzenetet"; /* member role */ "moderator" = "moderátor"; @@ -4422,6 +4431,12 @@ swipe action */ /* No comment provided by engineer. */ "request to join rejected" = "csatlakozási kérés elutasítva"; +/* rcv group event chat item */ +"requested connection" = "partneri kapcsolatot kért"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "a(z) %@ nevű csoportból partneri kapcsolatot kért"; + /* chat list item title */ "requested to connect" = "függőben lévő kapcsolat"; @@ -5430,6 +5445,9 @@ report reason */ /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Ez a beállítás csak az Ön jelenlegi **%@** nevű csevegési profiljában lévő üzenetekre vonatkozik."; +/* No comment provided by engineer. */ +"This setting is for your current profile **%@**." = "Ez a beállítás csak a jelenlegi **%@** nevű csevegési profiljára vonatkozik."; + /* No comment provided by engineer. */ "Time to disappear is set only for new contacts." = "Az üzeneteltűnési idő csak az új partnerekre vonatkozik."; @@ -6090,7 +6108,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "A jelentéseket megtekintheti a „Csevegés az adminisztrátorokkal” menüben."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Nem lehet üzeneteket küldeni!"; /* chat item text */ diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 7165a63668..0fe22cf12f 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -1410,6 +1410,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact preferences" = "Preferenze del contatto"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "Richieste di contatto dai gruppi"; + /* No comment provided by engineer. */ "contact should accept…" = "il contatto dovrebbe accettare…"; @@ -2367,6 +2370,9 @@ chat item action */ /* No comment provided by engineer. */ "Error sending message" = "Errore nell'invio del messaggio"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Errore impostando l'accettazione automatica"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Errore nell'impostazione delle ricevute di consegna!"; @@ -3248,6 +3254,9 @@ snd error text */ /* item status text */ "Member inactive" = "Membro inattivo"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "Il membro è eliminato - impossibile accettare la richiesta"; + /* chat feature */ "Member reports" = "Segnalazioni dei membri"; @@ -4422,6 +4431,12 @@ swipe action */ /* No comment provided by engineer. */ "request to join rejected" = "richiesta di entrare rifiutata"; +/* rcv group event chat item */ +"requested connection" = "connessione richiesta"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "connessione richiesta dal gruppo %@"; + /* chat list item title */ "requested to connect" = "richiesto di connettersi"; @@ -5430,6 +5445,9 @@ report reason */ /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Questa impostazione si applica ai messaggi del profilo di chat attuale **%@**."; +/* No comment provided by engineer. */ +"This setting is for your current profile **%@**." = "Questa impostazione è per il tuo profilo attuale **%@**."; + /* No comment provided by engineer. */ "Time to disappear is set only for new contacts." = "Il tempo di scomparsa è impostato solo per i contatti nuovi."; @@ -6090,7 +6108,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "Puoi vedere le tue segnalazioni nella chat con gli amministratori."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Non puoi inviare messaggi!"; /* chat item text */ diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 2c93438a8f..818b2eb1ce 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -178,15 +178,15 @@ /* time interval */ "%d sec" = "%d 秒"; +/* delete after time */ +"%d seconds(s)" = "%d 秒"; + /* integrity error chat item */ "%d skipped message(s)" = "%d 件のスキップされたメッセージ"; /* time interval */ "%d weeks" = "%d 週"; -/* No comment provided by engineer. */ -"%lld" = ""; - /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; @@ -283,6 +283,9 @@ time interval */ time interval */ "1 week" = "1週間"; +/* delete after time */ +"1 year" = "1年"; + /* No comment provided by engineer. */ "1-time link" = "使い捨てリンク"; @@ -322,6 +325,9 @@ time interval */ /* No comment provided by engineer. */ "Abort changing address?" = "アドレス変更を中止しますか?"; +/* No comment provided by engineer. */ +"About operators" = "オペレーターについて"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "SimpleX Chat について"; @@ -334,9 +340,21 @@ alert action swipe action */ "Accept" = "承諾"; +/* alert action */ +"Accept as member" = "メンバーとして承認する"; + +/* alert action */ +"Accept as observer" = "オブザーバーとして承認する"; + +/* No comment provided by engineer. */ +"Accept conditions" = "条件に同意する"; + /* No comment provided by engineer. */ "Accept connection request?" = "接続要求を承認?"; +/* alert title */ +"Accept contact request" = "連絡先リクエストを受け入れる"; + /* notification body */ "Accept contact request from %@?" = "%@ からの連絡要求を受け入れますか?"; @@ -344,9 +362,15 @@ swipe action */ swipe action */ "Accept incognito" = "シークレットモードで承諾"; +/* alert title */ +"Accept member" = "メンバーを承認する"; + /* call status */ "accepted call" = "受けた通話"; +/* No comment provided by engineer. */ +"Accepted conditions" = "承諾された条件"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。"; @@ -3459,7 +3483,7 @@ chat item action */ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "メッセージの書式にmarkdownを使用することができます:"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "メッセージを送信できませんでした!"; /* chat item text */ diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index c5dedbd0d4..92959a3bfe 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -5898,7 +5898,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "U kunt uw rapporten bekijken in Chat met beheerders."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Je kunt geen berichten versturen!"; /* chat item text */ diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 4e8a26d05c..b45d753b3c 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -5156,7 +5156,7 @@ chat item action */ /* alert message */ "You can view invitation link again in connection details." = "Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Nie możesz wysyłać wiadomości!"; /* chat item text */ diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index d797ebfbbd..93c3bedd4f 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -6108,7 +6108,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "Вы можете найти Ваши жалобы в Чате с админами."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Вы не можете отправлять сообщения!"; /* chat item text */ diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 03c1bdfed1..ef5d68b0fb 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -3168,7 +3168,7 @@ chat item action */ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "คุณสามารถใช้มาร์กดาวน์เพื่อจัดรูปแบบข้อความ:"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "คุณไม่สามารถส่งข้อความได้!"; /* chat item text */ diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index e93e824921..0dc37ccb57 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -1410,6 +1410,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact preferences" = "Kişi tercihleri"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "Gruplardan gelen iletişim talepleri"; + /* No comment provided by engineer. */ "contact should accept…" = "kişi kabul etmeli…"; @@ -1482,6 +1485,9 @@ set passcode view */ /* No comment provided by engineer. */ "Create SimpleX address" = "SimpleX adresi oluştur"; +/* No comment provided by engineer. */ +"Create your address" = "Adresinizi oluşturun"; + /* No comment provided by engineer. */ "Create your profile" = "Profilini oluştur"; @@ -2007,6 +2013,9 @@ chat item action */ /* No comment provided by engineer. */ "Enable camera access" = "Kamera erişimini etkinleştir"; +/* No comment provided by engineer. */ +"Enable disappearing messages by default." = "Varsayılan olarak kaybolan mesajları etkinleştirin."; + /* No comment provided by engineer. */ "Enable Flux in Network & servers settings for better metadata privacy." = "Daha iyi meta veri gizliliği için Ağ & sunucu ayarlarında Flux'u etkinleştirin."; @@ -6033,7 +6042,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "Raporlarınızı Yöneticilerle Sohbet bölümünde görüntüleyebilirsiniz."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Mesajlar gönderemezsiniz!"; /* chat item text */ diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 6c4cdf1510..daa954b2fb 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -6090,7 +6090,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "Ви можете переглянути свої звіти у чаті з адміністраторами."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Ви не можете надсилати повідомлення!"; /* chat item text */ diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 5ba26b8363..933286ccd5 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -5507,7 +5507,7 @@ chat item action */ /* alert message */ "You can view invitation link again in connection details." = "您可以在连接详情中再次查看邀请链接。"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "您无法发送消息!"; /* chat item text */ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml index ebd4b05e71..384a885e3b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -1201,8 +1201,8 @@ Ще бъдете свързани, когато заявката ви за връзка бъде приета, моля, изчакайте или проверете по-късно! Ще бъдете свързани, когато устройството на вашия контакт е онлайн, моля, изчакайте или проверете по-късно! Няма да загубите контактите си, ако по-късно изтриете адреса си. - Вашите настройки - Вашият адрес в SimpleX + Настройки + SimpleX адрес Използвай за нови връзки Вашите XFTP сървъри Използвай сървърите на SimpleX Chat\? @@ -1260,7 +1260,7 @@ Вие контролирате своя чат! Грешна парола! Гласови съобщения - Вашите настройки + Настройки Какво е новото Вашите контакти могат да позволят пълното изтриване на съобщението. Актуализирай паролата на базата данни @@ -1376,7 +1376,7 @@ Грешка при създаване на контакт с член Изпрати лично съобщение за свързване изпрати за свързване - свързан директно + заявка за връзка Разшири Блокиране на членове на групата Изпрати отново заявката за свързване? @@ -2171,7 +2171,7 @@ Вижте актуализираните условия Xiaomi устройства : моля, активирайте Autostart в системните настройки, за да работят известията.]]> Няма съобщение - Или да се сподели лично + Или сподели лично криптирани от край до край, с постквантова сигурност в директните съобщения.]]> Без фонова услуга Проверявай за съобщения на всеки 10 минути @@ -2504,4 +2504,8 @@ Сподели стар линк Линкът ще бъде кратък и профилът на групата ще бъде споделен чрез него. Обнови групов линк + ЗАЯВКИ ЗА КОНТАКТ ОТ ГРУПИ + Членът е изтрит - не може да се приеме заявката + заявка за връзка от група %1$s + Тази настройка е за текущия профил diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml index 57891501f8..f106e8085b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -524,7 +524,7 @@ Confirmeu el nou mot de pas… Voleu canviar la frase de pas per a la base de dades? connectat - connectat directament + connexió sol·licitada canviant d\'adreça per %s… connectat complet @@ -2477,4 +2477,8 @@ Compartir l\'enllaç antic L\'enllaç serà curt i el perfil del grup es compartirà a través d\'ell. Actualitzar l\'enllaç del grup + SOL·LICITUDS DE CONTACTE DE GRUPS + Membre eliminat(da); no es pot acceptar la sol·licitud. + connexió sol·licitada del grup %1$s + Aquesta configuració és per al perfil actual diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index 334deb0490..c369ab6b8d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -2432,4 +2432,22 @@ Otevřít chat Otevřít nový chat Otevřít novou skupinu + Připojit + Připojte se rychleji! 🚀 + POŽADAVKY NA PŘIPOJENÍ ZE SKUPIN + kontakt by měl přijmout… + Vytvořit vaši adresu + Popis příliš dlouhý + Povolení mizících zpráv ve výchozím nastavení. + Chyba změny profilu + Chyba otevření chatu + Chyba otevření skupiny + Chyba odmítnutí žádosti o kontakt + Skupina + Připojit ke skupině + Udržujte chat čistý + Méně provozu na mobilních sítích. + Načítání profilu… + Člen je smazán - nemůže přijmout žádost + Nová skupinová role: Moderátor diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 2421337ce8..d8d710dd4d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -2543,7 +2543,7 @@ Änderung des Profils nicht möglich Wenn Sie nach dem Verbindungsversuch ein anderes Profil verwenden möchten, löschen Sie den Chat und verwenden Sie den Link erneut. Chat mit Administratoren - Chat mit Mitgliedern bevor sie beitreten. + Mit Mitgliedern chatten bevor sie beitreten. Schneller miteinander verbinden! 🚀 Weniger Datenverkehr in mobilen Netzen. Sobald Sie auf Verbinden tippen, erhalten Sie sofort eine Nachricht. @@ -2573,9 +2573,9 @@ 4 neue Sprachen für die Bedienoberfläche Katalanisch, Indonesisch, Rumänisch und Vietnamesisch - Dank unserer Nutzer! Ihre Adresse erstellen - Verschwindende Nachrichten per Voreinstellung aktiviert. - Halten Sie Ihre Chats übersichtlich - Geben Sie eine Profil-Biografie und eine Begrüßungsmeldung ein. + Verschwindende Nachrichten sind per Voreinstellung aktiviert. + Ihre Chats übersichtlich halten + Sie können eine Profil-Biografie und eine Begrüßungsmeldung eingeben. Ihre Adresse teilen Verkürzte SimpleX-Adresse Die Zeit bis zum Verschwinden wird nur für neue Kontakte eingestellt. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 18458e8ebc..4c92d8e7da 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -14,7 +14,7 @@ Añadir servidores mediante el escaneo de códigos QR. Añadir servidores predefinidos Todos los miembros del grupo permanecerán conectados. - Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite para tí. (24 horas) + Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite. (24 horas) Android Keystore se usará para almacenar la frase de contraseña de forma segura después de cambiarla o reiniciar la aplicación - permitirá recibir notificaciones. Permites a tus contactos enviar mensajes temporales Permites a tus contactos enviar mensajes de voz. @@ -304,7 +304,7 @@ Imagen guardada en la Galería El archivo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde. Enlace de invitación de un uso - Pegar el enlace recibido + Pega el enlace recibido Error al guardar perfil de grupo Salir sin guardar Archivo guardado @@ -443,8 +443,8 @@ Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes. Cómo afecta a la batería Instantánea - Unirte - Unirte en modo incógnito + Unirme + Unirme en modo incógnito indirecta (%1$s) Claro Activado @@ -495,7 +495,7 @@ Error en la entrega del mensaje Lo más probable es que este contacto haya eliminado la conexión contigo. Moderar - Unirte como %s + Unirme como %s Sólo se pueden enviar 10 imágenes al mismo tiempo ¡Archivo grande! Silenciar @@ -518,7 +518,7 @@ OK (sólo almacenado por miembros del grupo) Ayuda sintaxis markdown - Servidores y Red + Servidores y Redes Se usarán hosts .onion si están disponibles. cursiva Llamada audio entrante @@ -715,7 +715,7 @@ envío no autorizado Escribe un nombre para el contacto Error desconocido - El rol cambiará a %s. Todos serán notificados. + El rol cambiará a %s. Se notificará en el grupo. La seguridad de SimpleX Chat ha sido auditada por Trail of Bits. Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido. Mensajes de chat SimpleX @@ -924,7 +924,7 @@ No tienes chats El contacto ha enviado un archivo mayor al máximo admitido (%1$s ). %1$d mensaje(s) omitido(s) - Dejarás de recibir mensajes de este grupo. El historial del chat se conservará. + Dejarás de recibir mensajes del grupo. El historial del chat se conservará. Mostrar código de seguridad Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos. ¡Mensajes de voz no permitidos! @@ -1424,7 +1424,7 @@ Grupo abierto Conexión finalizada (este dispositivo v%s)]]> - ¡Los mensajes de %s serán mostrados! + ¡Los mensajes nuevos de %s serán mostrados! Nombre de este dispositivo… Error Conectar con ordenador @@ -1534,7 +1534,7 @@ Añadir contacto Pulsa para escanear Guardar - Pulsa para pegar el enlace + Pulsa aquí para pegar el enlace Buscar o pegar enlace SimpleX Con uso reducido de batería. bloqueado por administrador @@ -1552,7 +1552,7 @@ ¿Bloqear miembro para todos? Creado: %s Bloquear para todos - ¿Desbloquear el miembro para todos? + ¿Desbloquear al miembro para todos? Desbloquear para todos bloqueado bloqueado por administrador @@ -1577,7 +1577,7 @@ El ordenador tiene un código de invitación incorrecto El ordenador ha sido desconectado estado desconocido - Migración de la base de datos en curso. \nPodría tardar varios minutos. + Migrando base de datos.\nPuede tardar varios minutos. El ordenador tiene una versión sin soporte. Por favor, asegúrate de usar la misma versión en ambos dispositivos el contacto %1$s ha cambiado a %2$s perfil actualizado @@ -2122,7 +2122,7 @@ Ajustes de dirección Crear enlace de un solo uso Para redes sociales - ¿Dirección SimpleX o enlace de un uso? + ¿Dirección SimpleX o enlace de un solo uso? Operadores de servidores Operadores de red Las condiciones de los operadores habilitados serán aceptadas después de 30 días. @@ -2143,7 +2143,7 @@ El operador del servidor ha cambiado. El protocolo del servidor ha cambiado. Barras de herramientas - Difuminar + Difuminado Navegación en el chat mejorada Descentralización de la red - El chat abre en el primer mensaje no leído.\n- Desplazamiento hasta los mensajes citados. @@ -2222,8 +2222,8 @@ El chat será eliminado para tí. ¡No puede deshacerse! Sólo los propietarios del chat pueden cambiar las preferencias. El miembro será eliminado del chat. ¡No puede deshacerse! - El rol cambiará a %s. Todos serán notificados. - Dejarás de recibir mensajes de este chat. El historial del chat se conserva. + El rol cambiará a %s. Se notificará en el chat. + Dejarás de recibir mensajes del chat. El historial del chat se conserva. Cómo ayuda a la privacidad Cuando está habilitado más de un operador, ninguno dispone de los metadatos para conocer quién se comunica con quién. Tu perfil de chat será enviado a los miembros de chat @@ -2359,7 +2359,7 @@ rechazado rechazado ¿Expulsar miembros? - ¡Los mensajes de estos miembros serán mostrados! + ¡Los mensajes nuevos de estos miembros serán mostrados! ¿Desbloquear los miembros para todos? ¡Todos los mensajes nuevos de estos miembros estarán ocultos! ¿Bloquear miembros para todos? @@ -2394,14 +2394,14 @@ has aceptado al miembro pendiente de revisión por revisar - Chat con los administradores + Chat con administradores Chat con miembro en revisión por los administradores %1$s aceptado desactivado Admisión de miembros el miembro usa una versión antigua - Sin chats con miembros + Sin chats Error al eliminar el chat con el miembro %d chats con miembros %d mensajes @@ -2411,19 +2411,19 @@ no se pueden enviar mensajes contacto eliminado contacto desactivado - el contacto no está listo + en espera de ser aceptado el grupo ha sido eliminado no sincronizado expulsado del grupo petición para unirse rechazada ¡No puedes enviar mensajes! - Puedes ver tus informes en Chat con los administradores + Puedes ver tus informes en Chat con administradores has salido te ha aceptado Un miembro nuevo desea unirse al grupo. todos Chat con miembros - Chat con los administradores + Chat con administradores Eliminar chat ¿Eliminar chat con el miembro? Rechazar @@ -2442,12 +2442,12 @@ Chatea con el miembro antes de unirse. Conectar ¡Conéctate más rápido! 🚀 - el contacto debe aceptar… + el contacto debe aceptarte… Error al cambiar el perfil Error al abrir el chat Error al abrir el grupo Error al rechazar la solicitud del contacto - Unirte al grupo + Unirme al grupo Menos tráfico en redes móviles. Tras pulsar Contactar, mensajea ya. Nuevo rol de grupo: Moderador @@ -2457,7 +2457,7 @@ Abrir grupo nuevo Abrir para aceptar Abrir para conectar - Abrir para unirte + Abre para unirte Timeout enrutamiento privado La dirección pasará a ser corta y tu perfil será compartido mediante la dirección. Timeout protocolo en segundo plano @@ -2491,7 +2491,7 @@ Grupo Pulsa Conectar para chatear Pulsa Conectar para enviar solicitud - Pulsa Unirte al grupo + Pulsa Unirme al grupo Mensajes temporales activados sólo para los contactos nuevos. Usar perfil incógnito Mi contacto empresarial diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index ebd2936312..4703958190 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -11,12 +11,12 @@ لغو تغییر نشانی تغییر نشانی را لغو می‌کنید؟ درباره سیمپل‌اکس(SimpleX) - به وسیله نشانی مخاطب متصل می‌شوید؟ - به وسیله لینک یک بار مصرف متصل می‌شوید؟ - از پروفایل ناشناس جدید استفاده کن + با نشانی مخاطب ارتباط برقرار شود؟ + با لینک یک بار مصرف ارتباط برقرار شود؟ + استفاده از پروفایل ناشناس جدید در حال باز کردن پایگاه داده… پروفایل شما به مخاطبی که این لینک را از او دریافت کردید، فرستاده خواهد شد. - متصل شدن به صورت ناشناس + اتصال ناشناس شما یک مسیر نامعتبر فایل به اشتراک گذاشتید. موضوع را به توسعه‌دهندگان برنامه گزارش دهید. هنوز از دریافت فایل پشتیبانی نمی‌شود قالب پیام نامعتبر @@ -30,7 +30,7 @@ در حال اتصال k به گروه می‌پیوندید؟ - از پروفایل کنونی استفاده کن + استفاده از پروفایل کنونی به تمام اعضای گروه متصل خواهید شد. متصل شدن مسیر نامعتبر فایل @@ -1001,14 +1001,14 @@ عضو پیشین %1$s بسط دادن انتخاب نقش عبارت عبور رمزنگاری پایگاه داده به‌روز خواهد شد. - مستقیما متصل شد + درخواست اتصال کرد %s متصل شد پیوستن به گروه به‌روزرسانی تایید عبارت عبور جدید… پایگاه داده رمزنگاری شود؟ نسخه پایگاه داده ناسازگار - پروفایل گروه به‌روز شد + پروفایل گروه به‌روز شده شما نشانی را برای %s تغییر دادید شما نشانی را تغییر دادید تجدید مذاکره رمزنگاری مجاز است @@ -2320,7 +2320,7 @@ پایگاه داده چت تکراری‌ها تغییر - Flux را در تنظیمات شبکه و سرورها فعال کنید تا حریم خصوصی متاداده بهتر شود. + Flux را در تنظیمات شبکه و سرورها فعال کنید تا حریم خصوصی فراداده بهتر شود. فعال کردن لاگ‌ها مذاکره مجدد رمزنگاری در حال انجام است. خطا @@ -2352,7 +2352,7 @@ اتصال اصلاح شود؟ اندازه فونت برای همه مدیران - برای بهبود حریم خصوصی متاداده. + برای بهبود حریم خصوصی فراداده. به عنوان مثال، اگر مخاطب شما پیام‌ها را از طریق یک سرور چت SimpleX دریافت کند، برنامه شما آن‌ها را از طریق یک سرور Flux تحویل خواهد داد. برای من برای مسیریابی خصوصی @@ -2508,4 +2508,8 @@ آدرس خود را به اشتراک بگذارید ۴ زبان جدید رابط کاربری کاتالان، اندونزیایی، رومانیایی و ویتنامی - با تشکر از کاربران ما! + عضو حذف شده است - نمی‌توان درخواست را قبول کرد + این تنظیمات برای پروفایل فعلی شماست + درخواست‌های تماس از گروه‌ها + درخواست اتصال از گروه %1$s diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index c6f89dd3c0..36b69f2f31 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -193,7 +193,7 @@ Kapcsolódás Közvetlenül kapcsolódik? Kapcsolódás - partnerkapcsolatot kért + partneri kapcsolatot kért kapcsolat %1$d a partner e2e titkosítással rendelkezik Csoport létrehozása véletlenszerű profillal. @@ -846,7 +846,7 @@ ÜZENETEK ÉS FÁJLOK tag Privát kapcsolat létrehozása - moderálva lett %s által + %s moderálta ezt az üzenetet Győződjön meg arról, hogy a fájl helyes YAML-szintaxist tartalmaz. Exportálja a témát, hogy legyen egy példa a témafájl szerkezetére. dőlt Érvénytelen a fájl elérési útvonala @@ -2481,6 +2481,6 @@ Régi hivatkozás megosztása PARTNERI KAPCSOLATKÉRÉSEK A CSOPORTOKBÓL A tag törölve lett – nem lehet elfogadni a kérést - a(z) %1$s nevű csoportból partnerkapcsolatot kért + a(z) %1$s nevű csoportból partneri kapcsolatot kért Ez a beállítás a jelenlegi profiljára vonatkozik diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 77d38c0824..5785f32de5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -1380,7 +1380,7 @@ Errore di creazione del contatto Invia messaggio diretto per connetterti invia per connettere - si è connesso/a direttamente + connessione richiesta Espandi Ripetere la richiesta di connessione? contatto eliminato @@ -2515,4 +2515,8 @@ Condividi il link vecchio Il link sarà breve e il profilo del gruppo verrà condiviso attraverso il link. Aggiorna il link del gruppo + RICHIESTE DI CONTATTO DAI GRUPPI + Il membro è eliminato - impossibile accettare la richiesta + connessione richiesta dal gruppo %1$s + Questa impostazione è per il tuo profilo attuale diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index dc15648e64..efc1810bac 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -2018,4 +2018,28 @@ 承諾 サーバオペレータは、プライベートチャット・グループ・連絡先にはアクセスできません。 SimpleX Chat を利用することで、以下の事項に同意したものと見なされます:\n- パブリックグループでは合法なコンテンツのみを送信すること。\n- 他のユーザを尊重すること、またスパムメッセージを送信しないこと。 + チャットを開く + 新しいチャットを開始 + 新しいグループを開始 + 無効なリンク + SimpleXのリンクが正しいか確認してください + あなたとモデレーターのみが見ることができます + 送信者とモデレーターのみが見ることができます + アーカイブされたレポート + %sによってアーカイブされたレポート + E2E暗号化.]]> + 接続を要求されました + 招待を受け入れました + SimpleXチャンネルのリンク + スパム + 不適切なコンテンツ + コミュニティガイドライン違反 + 不適切なプロフィール + 他の理由 + サーバーの保存中にエラーが発生しました + メッセージサーバーがありません + メッセージを受信するサーバーがありません + プライベートメッセージルーティング用のサーバーがありません。 + メディアおよびファイルサーバーは存在しません。 + ファイルを送信するサーバーがありません。 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml index 5d113ecb46..5f12e762aa 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -268,7 +268,7 @@ O contacto permite Preferências de conversa SimpleX - m + mil Conectar através do endereço de contacto? Conectar via link de convite? Conectar através da ligação do grupo\? @@ -971,8 +971,9 @@ Todos os perfis Já conectando! Já entrando no grupo! - %1$d arquivo(s) falharam o download. - %1$d erro(s) no arquivo:\n%2$s - %1$d arquivo(s) estão sendo baixados. - %1$d arquivo(s) foram deletados. + Não foi possível descarregar %1$d ficheiros(s). + %1$d erro(s) nos ficheiros:\n%2$s + Ainda a descarregar %1$d ficheiros(s). + %1$d ficheiros(s) eliminado(s). + Não foi possível descarregar %1$d ficheiro(s). diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index c67e843d07..56e8faf690 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -22,7 +22,7 @@ %1$s MEMBRI 1 zi 1 minut - Link de unică folosință + Link unic 5 minute Despre SimpleX Despre adresa SimpleX @@ -135,7 +135,7 @@ Aplică Creează profil Creează profil - Acceptă automat cererile de contactare + Acceptare automată a cererilor de contact personalizat În prezent dimensiunea maximă pentru fișiere este %1$s. Creează adresă SimpleX @@ -186,8 +186,8 @@ Restaurați Reîmprospătează Revocați fișierul? - Revocați - Renegociați criptarea? + Revocă + Renegociezi criptarea? Resetare Respinge Salvați fraza de acces în setări @@ -205,16 +205,16 @@ Salvați parola profilului Salvați fraza de acces și deschideți chatul %s și %s - Renegociați criptarea - Salvați și notificați contactul - Salvați și notificați contactele - Salvați și notificați membrii grupului + Renegociază criptarea + Salvează și notifică contactul + Salvează și notifică contactele + Salvează și notifică membrii grupului Rulează când aplicația este deschisă Răspunde - Revocați fișierul + Revocă fișierul Salvează Salvați setările? - Salvați preferințele? + Salvezi preferințele? Repornire Restaurați copia de rezervă a bazei de date Restaurați copia de rezervă a bazei de date? @@ -232,7 +232,7 @@ Salvați fraza de acces în Keystore Salvați profilul grupului Repetă - Trimiteți previzualizări de linkuri + Trimite previzualizări de linkuri Setați frază de acces Partajează adresa Trimis la @@ -297,7 +297,7 @@ setați o nouă poză de profil Trimis la: %s SERVERE - Trimiteți mesaj live + Trimite mesaj în direct %s descărcat Partajați adresa cu contactele? Afișează previzualizare @@ -321,7 +321,7 @@ SimpleX nu poate rula în fundal. Vei primi notificări doar atunci când aplicația rulează. Serviciul SimpleX Chat Adresă SimpleX - Oprește + Oprire Linkuri SimpleX Linkurile SimpleX sunt interzise. Securitatea chatului SimpleX a fost auditată de Trail of Bits. @@ -359,7 +359,7 @@ Folosește mereu releu Aplică pentru Începe o nouă conversație - Stea pe GitHub + Acordă o stea pe GitHub criptare standard end-to-end Pornește periodic Mereu @@ -421,7 +421,7 @@ Contactul nu este conectat încă! Conectare prin link Puteți vedea din nou linkul de invitație în detaliile conexiunii. - Profilurile tale de chat + Profilurile tale de conversație Creează profil de chat apel încheiat %1$s Creează @@ -520,7 +520,7 @@ Te conectezi la tine? Verifică conexiunea la internet și încearcă din nou Culori conversație - Temă pentru chat + Tema conversației Bun pentru baterie. Aplicația verifică mesajele la fiecare 10 minute. Pot fi pierdute apeluri sau mesaje urgente.]]> Negru Blocați membrul pentru toți? @@ -545,7 +545,7 @@ Versiunea aplicației: %s pentru fiecare profil de conversație pe care le aveți în aplicație]]> Permiteți downgrade-ul - pentru fiecare contact și membru de grup \nVa rugăm considerați că: dacă aveți prea multe conexiuni, consumul dumneavoastră de baterie și trafic de internet pot fi considerabil mai mari, iar unele conexiuni pot eșua.]]> + pentru fiecare contact și membru al grupului.\nRețineți: dacă aveți multe conexiuni, consumul bateriei și al traficului de date poate crește considerabil, iar unele conexiuni pot eșua.]]> Conexiune terminată Se conectează la desktop S-a solicitat primirea imaginii @@ -554,7 +554,7 @@ conectat Confirmați codul de access Confirmare actualizare bază de date - "schimbat rolul lui %s la %s" + a schimbat rolul lui %s în %s conectat se conectează (introdus) conectat @@ -586,13 +586,13 @@ Șterge conversația Conectare Consolă conversație - Confirmați parola + Confirmă parola colorat Toate contactele, conversațiile și fișierele dvs. vor fi criptate securizat și încărcate în fragmente către releele XFTP configurate. S-a solicitat primirea videoclipului Comparați fișierul Vă rugăm să rețineți: nu veți putea recupera sau schimba parola dacă o veți pierde.]]> - schimbat rolul dumneavoastră la %s + rolul tău a fost schimbat în %s conectat se conectează conectat @@ -600,7 +600,7 @@ Buton de închidere Ștergeți verificarea Configurare servere ICE - conectat direct + conexiune solicitată se conectează (anunțat) Cel mai bun pentru baterie. Veți primi notificări doar când aplicația rulează (FĂRĂ servicii de fundal).]]> Se conectează apelul @@ -634,7 +634,7 @@ Eroare de decriptare Șterge imaginea Șterge după - Ștergeți pentru toată lumea + Șterge pentru toată lumea Descriere Șterge fișierul Șterge coada @@ -645,12 +645,12 @@ %d zile %d zi Șterge adresa - Ștergeți mesajele + Șterge mesajele Ștergeți %d mesaje? Ștergi adresa? Șterge toate fișierele Șterge linkul - Ștergeți linkul? + Ștergi linkul? zile Șterge Ștergeți mesajul? @@ -659,7 +659,7 @@ Ștergeți serverul Șterge baza de date contact șters - Ștergeți grupul? + Ștergi grupul? Șterge baza de date de pe acest dispozitiv Șterge profil de conversație Șterge fișierele pentru toate profilurile de chat @@ -685,7 +685,7 @@ Verifică pentru actualizări Creează Estompează media - BAZĂ DE DATE DISCUȚIE + BAZĂ DE DATE CONVERSAȚIE Conectează-te cu prietenii mai ușor. încercări Completat @@ -717,13 +717,13 @@ Se conectează la contact, vă rugăm să așteptați sau să verificați mai târziu! Setări avansate Setări - Setările tale + Setări apel audio criptat e2e apel video criptat e2e DISPOZITIV EXPERIMENTAL Criptează - erori la descifrare + erori de decriptare TU nicio criptare e2e criptat e2e @@ -747,7 +747,7 @@ Prin browser Spam Conținut inadecvat - Încălcă normelor comunitare + Încălcarea liniilor directoare ale comunității Profil inadecvat Alt motiv Eroare la salvarea serverelor SMP @@ -761,7 +761,7 @@ Eroare de renegociere a criptării invitat să se conecteze incognito prin linkul adresei de contact - printr-un link de unică folosință + prin link de unică folosință 1 raport 1 an redirecționat @@ -819,9 +819,9 @@ Importați baza de date de chat? Ștergeți raportul Redirecționați mesajele… - Ștergeți doar conversația + Șterge doar conversația Păstrezi invitația nefolosită? - Introduceți numele dumneavoastră: + Introdu numele tău: Acordați permisiunea (permisiunile) de a efectua apeluri Notificări și baterie Deschide @@ -948,7 +948,7 @@ Editează Info Redirecționat de la - Șterge %d mesaje de la membri? + Ștergi cele %d mesaje ale membrilor? Rapoarte de membri Fișierele și conținutul media sunt interzise! Fișierul nu a fost găsit @@ -1037,13 +1037,13 @@ Server nou Asigurați -vă că adresele Serverului WebRTC ICE sunt în format corect, separate pe linii și nu sunt duplicate. Rutarea mesajelor - Editați imaginea + Editează imaginea Deschide setările Acordare în setări Instant Cum afectează bateria Activare (păstrați suprascrierile) - Activați codul de autodistrugere + Activează codul de autodistrugere MEMBRU Operator de rețea Desktop-uri conectate @@ -1077,7 +1077,7 @@ Acordare permisiuni Cască Închide - Criptați fișierele locale + Criptează fișierele locale Blochează după Mod de blocare Cod de acces nou @@ -1091,7 +1091,7 @@ indirect (%1$s) Remedierea nu este acceptată de membrul grupului Operator - Faceți profilul privat! + Fă profilul privat! Deschideți contiții Nu mai afișa Permite să aibă multe conexiuni anonime fără date partajate între ele într -un singur profil de chat. @@ -1114,7 +1114,7 @@ %d conversații cu membri activat Conexiunea a atins limita de mesaje nelivrate, este posibil ca persoana de contact să fie offline. - Dezactivați SimpleX Lock + Dezactivează blocarea SimpleX Eroare la salvarea setărilor Deconectezi desktopul? Nu activați @@ -1131,7 +1131,7 @@ Eroare la ștergerea conexiunii de contact în așteptare criptare agreată pentru %s Introduceți mesajul de bun venit… (opțional) - Criptați fișierele stocate și media + Criptează fișierele și conținutul media stocat Eroare la acceptarea condițiilor Eroare la activarea confirmărilor de livrare! Eroare fișier @@ -1169,7 +1169,7 @@ Invitați la conversație Nou rol de membru Invitați membrii - Editați profilul de grup + Editează profilul grupului Grupul va fi șters pentru dvs. - această acțiune nu poate fi anulată! Membrul va fi eliminat din grup - acest lucru nu poate fi anulat! Membrii vor fi eliminați din chat - acest lucru nu poate fi anulat! @@ -1288,7 +1288,7 @@ Vă protejează adresa și conexiunile IP. Mod luminos Sursa mesajelor rămâne privată. - Mesaje live + Mesaje în direct Asigurați-vă că configurația proxy este corectă. Cel mai probabil acest contact a șters conexiunea cu dvs. Avertizare de livrare a mesajelor @@ -1313,7 +1313,7 @@ profilul grupului a fost actualizat proprietar Șterge conversația - ieșit + părăsit Remedierea nu este suportată de contact Erori Deschide grupul @@ -1333,7 +1333,7 @@ Află mai multe Invită Eroare la inițializarea WebView. Asigurați-vă că aveți WebView instalat și că arhitectura sa suportată este arm64.\nEroare: %s - Faceți o conexiune privată + Stabilește o conexiune privată Deschide SimpleX Chat pentru a accepta apelul Activați blocarea Se poate întâmpla atunci când tu sau conexiunea ta ați folosit backupul vechi al bazei de date. @@ -1416,7 +1416,7 @@ Reacții la mesaje expirat Descărcați %s (%s) - Instalați update + Instalează actualizarea Nu crea adresă De exemplu, dacă persoana de contact primește mesaje prin intermediul unui server SimpleX Chat, aplicația le va livra prin intermediul unui server Flux. Deschide Setările Safari / Site-uri web / Microfon, apoi selectează Permite pentru localhost. @@ -1452,7 +1452,7 @@ Descoperă și alătură-te grupurilor Chiar și atunci când este dezactivat în conversație. - livrare mai stabilă a mesajelor.\n- grupuri mai bune.\n- și multe altele! - Face ca un mesaj să dispară + Fă un mesaj să dispară Migrați către un alt dispozitiv prin codul QR. Ștergeți până la 20 de mesaje odată. Vrei să te alături grupului tău? @@ -1460,7 +1460,7 @@ Notificări instant! Deschide setările aplicației %d secunde - Activați SimpleX Lock + Activează blocarea SimpleX Eroare la afișarea notificării, contactați dezvoltatorii. Eroare de livrare a mesajelor Eroare server destinație: %1$s @@ -1487,12 +1487,12 @@ Notificări Doar proprietarii grupului pot activa mesajele vocale. Editați - Mesaj live! + Mesaj în direct! Fără sunet toate OK Previzualizare imagine din link Conversație nouă - Link invitație unică + Link de invitație unic Servere mesaje Alte servere SMP Introduceți serverul manual @@ -1513,14 +1513,14 @@ niciodată Niciun fișier primit sau trimis Frază de acces nouă… - Criptați baza de date? + Criptezi baza de date? Introduceți fraza de acces corectă. Vrei să te alături grupului? Migrații: %s Alătură-te Părăsiți Părăsiți conversația? - ieșit + a părăsit invitat prin linkul dvs. de grup Un membru nou vrea să se alăture grupului. criptare agreată @@ -1580,7 +1580,7 @@ Exportă tema Importați tema Eroare la importul temei - Eroare la acceptarea solicitării de contact + Eroare la acceptarea cererii de contact Descărcați fișierul Eroare la ștergerea contactului Eroare la crearea adresei @@ -1628,7 +1628,7 @@ Servere presetate Vă rugăm să o rețineți sau să o păstrați în siguranță - nu există nicio modalitate de a recupera o parolă pierdută! Confidențialitate și securitate - Protejați ecranul aplicației + Protejează ecranul aplicației Imagini de profil Vă rugăm să stocați în siguranță fraza de acces, NU veți putea accesa chatul dacă o pierdeți. Interziceți trimiterea de mesaje vocale. @@ -1739,7 +1739,7 @@ Resetare Rapoarte informații despre coada serverului: %1$s\n\nultimul mesaj primit: %2$s - Salvați și reconectați + Salvează și reconectează-te Raportați altceva: doar moderatorii grupului îl vor vedea. caută Setați numele chatului… @@ -1752,7 +1752,7 @@ Examinați condițiile Raport Adresa de primire va fi schimbată la un alt server. Schimbarea adresei se va finaliza după ce expeditorul se conectează online. - Salvați setările pentru admitere? + Salvezi setările de acces? Revizuiți mai târziu Trimiterea confirmărilor este dezactivată pentru %d grupuri recenzie @@ -1802,7 +1802,7 @@ Selectează profilul de chat Salvați setările de acceptare automată Salvați lista - TRIMITEȚI CONFIRMĂRI DE LIVRARE LA + TRIMITE CONFIRMĂRI DE LIVRARE LA Parola de autodistrugere a fost schimbată! Înregistrare actualizată la Selectează operatorii de rețea de utilizat. @@ -1851,7 +1851,7 @@ Cheie greșită sau adresă necunoscută a fragmentului de fișier - cel mai probabil fișierul este șters. Prea multe videoclipuri! Videoclip trimis - Opriți partajarea + Oprește partajarea Servere ICE WebRTC Actualizați și deschideți chatul Fișiere încărcate @@ -1904,7 +1904,7 @@ Folosește acreditări proxy diferite pentru fiecare conexiune. Adresă SimpleX sau link unic? Pentru a vă proteja confidențialitatea, SimpleX folosește ID-uri separate pentru fiecare dintre contactele dvs. - Ești invitat(ă) în grup + Ai fost invitat în grup Pentru a primi Utilizare pentru fișiere Pentru a trimite @@ -1933,7 +1933,7 @@ necunoscut Poți ascunde sau dezactiva notificările unui profil de utilizator – ține apăsat pentru meniu. Ați permis - Partajează link cu utilizare unică cu un prieten + Partajează linkul unic cu un prieten Mesaje vocale Mesaj vocal… Setări proxy SOCKS @@ -1946,11 +1946,11 @@ Mesajele vocale sunt interzise! Ați acceptat conexiunea Partajează adresa public - Partajează acest link de invitație cu utilizare unică + Partajează acest link de invitație unic Pentru a verifica criptarea end-to-end cu contactul dvs., comparați (sau scanați) codul de pe dispozitivele dvs. Utilizare pentru conexiuni noi Utilizați proxy SOCKS - Opriți partajarea adresei? + Oprești partajarea adresei? Când aplicația rulează Aplicația vă protejează confidențialitatea utilizând operatori diferiți în fiecare conversație. Vizualizați condițiile @@ -1997,7 +1997,7 @@ Fără Tor sau VPN, adresa ta IP va fi vizibilă pentru aceste retransmiteri XFTP:\n%1$s. Actualizați parola bazei de date Opriți trimiterea fișierului? - lovitură + tăiere Viitorul mesageriei Pentru a efectua apeluri, permiteți utilizarea microfonului. Încheiați apelul și încercați să sunați din nou. Când sunt activați mai mulți operatori, niciunul dintre ei nu are metadate pentru a afla cine comunică cu cine. @@ -2093,13 +2093,13 @@ Deconectați desktopul? Se așteaptă desktopul… Prea multe imagini! - Încărcați fișierul + Încarcă fișier Atinge butonul Atingeți pentru a scana (pentru a partaja cu persoana de contact) Pentru a începe o nouă conversație Video - Partajează link cu utilizare unică + Partajează link unic Acest link nu este un link de conectare valid! Acest cod QR nu este un link! Link scurt @@ -2161,7 +2161,7 @@ Raportul va fi arhivat pentru dvs. Atingeți Creează o adresă SimpleX în meniu pentru a o crea mai târziu. Acest text este disponibil în setări - ești invitat în grup + Ești invitat în grup Se așteaptă imaginea Se așteaptă imaginea Se așteaptă videoclipul @@ -2174,7 +2174,7 @@ Partajează adresa SimpleX pe rețelele de socializare. Pentru a se conecta, contactul dvs. poate scana codul QR sau poate folosi linkul din aplicație. Puteți seta numele conexiunii, pentru a vă aminti cu cine a fost partajat linkul. - Adresa SimpleX și linkurile unice pot fi partajate în siguranță prin orice serviciu de mesagerie. + Adresa SimpleX și linkurile unice pot fi partajate în siguranță prin orice aplicație de mesagerie. Codul scanat nu este un cod QR de tip link SimpleX. Server de testare Servere de testare @@ -2235,7 +2235,7 @@ Bucăți șterse Serverele tale ICE ai eliminat %1$s - Preferințele dumneavoastră + Preferințele tale Adaugă listă Toate Puteți vizualiza în continuare conversația cu %1$s în lista de conversații. @@ -2245,7 +2245,7 @@ Conexiune blocată Conexiunea este blocată de operatorul serverului:\n%1$s. criptate end-to-end, cu securitate post-cuantică în mesajele directe.]]> - Bare de instrumente ale aplicației + Bare de instrumente Securitate îmbunătățită ✅ Toate conversațiile vor fi eliminate din lista %s, iar lista va fi ștearsă Conexiunea dvs. a fost mutată la %s, dar a apărut o eroare neașteptată la redirecționarea către profil. @@ -2257,20 +2257,20 @@ Întreabă %s.]]> Schimbați ștergerea automată a mesajelor? - Adresă sau link de unică folosință? + Adresă sau link unic? Sesiune de aplicație - Contactele dvs. - Este posibil ca datele dumneavoastră de autentificare să fie trimise necriptate. + Contactele tale + Datele tale de conectare pot fi trimise necriptat. Adresă de afaceri Adăugați membrii echipei dvs. la conversații. Verifică mesajele la fiecare 10 minute %s, acceptați condițiile de utilizare.]]> - Confirmați autentificarea dvs + Confirmă-ți datele de autentificare Schimbă profilurile de chat Contacte Conexiunea necesită renegocierea criptării. Adresa ta SimpleX - Creează link de unică folosință + Creează link unic Profilul tău actual Aplicația rulează întotdeauna în fundal acceptat %1$s @@ -2282,8 +2282,8 @@ Arhivează contactele pentru a discuta mai târziu. %1$s.]]> Ai partajat o cale de fișier nevalidă. Raportează problema dezvoltatorilor aplicației. - Deschideți în aplicația mobilă, apoi atingeți Conectați-vă în aplicație.]]> - Adaugă link scurt + Deschide în aplicația mobilă, apoi atinge Conectare în aplicație.]]> + Actualizează adresa Configurați operatorii serverului Condițiile vor fi acceptate pentru operatorii activați după 30 de zile. Faceți clic pe butonul de informații de lângă câmpul de adresă pentru a permite utilizarea microfonului. @@ -2295,10 +2295,10 @@ Poți partaja un link sau un cod QR - oricine se va putea alătura grupului. Nu vei pierde membrii grupului dacă îl ștergi ulterior. Blocați membrii pentru toți? Chat - %s.]]> + %s.]]> Condiții acceptate pe: %s. Condițiile vor fi acceptate automat pentru operatorii activați pe: %s. - %s.]]> + %s.]]> %s.]]> %s.]]> Condiții de utilizare @@ -2320,7 +2320,7 @@ Adaugă prieteni Adaugă membri echipei Chatul există deja! - Deschideți în aplicația mobilă.]]> + Deschide în aplicația mobilă.]]> Continuă %s a fost deconectat]]> Despre operatori @@ -2338,7 +2338,7 @@ invitație acceptată Puteți să vă împărtășiți adresa ca link sau cod QR - oricine se poate conecta la dvs. Acceptă - Confidențialitatea dumneavoastră + Confidențialitatea ta Acceptă condițiile Zoom %1$s.]]> @@ -2369,10 +2369,10 @@ %s cu motivul: %s]]> %s a fost deconectat]]> (nou)]]> - Serverul dumneavoastră + Serverul tău Trebuie să îi permiți contactului tău să te sune pentru a-l putea suna. Adresa serverului dvs. - Serverele dumneavoastră + Serverele tale Vei fi conectat la grup atunci când dispozitivul gazdei grupului va fi online, te rugăm să aștepți sau să verifici mai târziu! Tu decizi cine se poate conecta. Vei fi conectat când cererea ta de conectare va fi acceptată, te rugăm să aștepți sau să verifici mai târziu! @@ -2380,7 +2380,7 @@ Experiență de utilizare îmbunătățită 1 chat cu un membru Toate rapoartele vor fi arhivate pentru dvs. - repozitoriul nostru GitHub.]]> + depozitul nostru GitHub.]]> Permiteți în următoarea fereastră de dialog să primească notificări instantaneu.]]> Setări adresă Arhivare rapoarte @@ -2388,10 +2388,10 @@ toți Afaceri Prin utilizarea SimpleX Chat sunteți de acord să:\n- trimiteți doar conținut legal în grupurile publice.\n- respectați ceilalți utilizatori – fără spam. - cu un singur contact - partajați personal sau prin orice mesagerie.]]> + doar cu un singur contact - partajează-l personal sau prin orice altă aplicație de mesagerie.]]> Nu trebuie să utilizați aceeași bază de date pe două dispozitive. arătați codul QR în apelul video sau distribuiți linkul.]]> - Utilizează de pe desktop în aplicația mobilă și scanați codul QR.]]> + Utilizare de pe desktop în aplicația mobilă și scanează codul QR.]]> SimpleX rulează în fundal în loc să utilizeze notificări push.]]> Utilizarea bateriei aplicației / Nerestricționat în setările aplicației.]]> Bucăți descărcate @@ -2428,4 +2428,26 @@ Deschide pentru conectare Deschide pentru a te alătura Timp de așteptare depășit pentru rutarea privată + Te poți conecta la conversație și poți trimite mesaje imediat ce apeși pe Conectare. + Rol nou în grup: Moderator + Descriere scurtă: + Trimis contactului tău după conectare. + Actualizezi la o adresă permanentă? + Mesaj de bun venit + SOLICITĂRI DE CONTACT DE LA GRUPURI + Această setare este pentru profilul tău actual + Partajează adresa veche + Partajează linkul vechi + Se încarcă profilul… + cerere de conectare din grupul %1$s + Eroare la deschiderea conversației + Eroare la deschiderea grupului + Mai puțin trafic pe rețelele mobile. + Adresa va fi scurtă, iar profilul tău va fi partajat prin intermediul acestei adrese. + Actualizează + Adresă SimpleX scurtă + Atinge Conectare pentru a începe conversația + Creează-ți adresa + Acceptă cererea de contact + doar după ce cererea ta este acceptată.]]> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index a13106edaf..8fe730fab1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -372,7 +372,7 @@ Проверьте адрес сервера и попробуйте снова. Удалить сервер SimpleX Chat для терминала - Поставить звёздочку в GitHub + Поставить звёздочку на GitHub Внести свой вклад Оценить приложение Использовать серверы предосталенные SimpleX Chat? @@ -1463,7 +1463,7 @@ Послать прямое сообщение контакту Ошибка отправки приглашения Отправьте сообщение чтобы соединиться - соединен напрямую + запрос на соединение Раскрыть Блокируйте членов группы Повторить запрос на соединение? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 436f80a8fe..43e67bb51b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -2502,4 +2502,5 @@ Biyografi çok uzun Açıklama çok büyük Kaybolma süresi yalnızca yeni kişiler için ayarlanır. + Gizli profil kullan diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index 0922ed5d55..ee0c785e9f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -2075,4 +2075,54 @@ 更好的訊息日期。 為了更好的元資料隱私。 你可以再試一次。 + 使用匿名個人檔案 + 開啟聊天 + 連接 + 升級 + 群組 + 可見的紀錄 + 慢速函數 + 有線以太網 + 桌布背景 + 桌布輔色 + 訂閱已忽略 + 訂閱錯誤 + 代理身份驗證 + 已靜音 + 未送達的訊息 + 沒有訊息 + 預設伺服器 + 審閱使用條款 + %s 個伺服器 + 檢視使用條款 + 接收 + 開啟使用條款 + 開啟變更 + 網路去中心化 + 離開聊天? + 離開聊天 + 啟用日誌 + 沒有聊天 + 成員舉報 + 僅自己 + 舉報:%s + 待批准 + 已更新使用條款 + 預設伺服器 + 加入群組 + 增加訊息 + 傳送請求 + 你的個人資料 + 歡迎訊息 + 升級地址? + 正在載入個人資料… + 你的簡介: + 簡短描述: + 你的聯絡人 + 你的群組 + 商業連接 + 顯示最近的訊息 + 簡化的匿名模式 + 點擊以連接 + 從桌面使用 From b4293e361be1f997e09960237450ad442c5053bb Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 8 Aug 2025 07:45:39 +0100 Subject: [PATCH 09/11] website: translations (#6162) * Translated using Weblate (Persian) 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/fa/ * Translated using Weblate (Persian) 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/fa/ --------- Co-authored-by: webamooz --- website/langs/fa.json | 260 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 259 insertions(+), 1 deletion(-) diff --git a/website/langs/fa.json b/website/langs/fa.json index 0967ef424b..5fce03d04a 100644 --- a/website/langs/fa.json +++ b/website/langs/fa.json @@ -1 +1,259 @@ -{} +{ + "home": "خانه", + "developers": "توسعه‌دهندگان", + "reference": "مرجع", + "blog": "بلاگ", + "features": "ویژگی‌ها", + "why-simplex": "چرا SimpleX", + "simplex-privacy": "حریم خصوصی SimpleX", + "simplex-network": "شبکه SimpleX", + "simplex-explained": "توضیحات در مورد SimpleX", + "simplex-explained-tab-1-text": "۱. تجربه کاربران", + "simplex-explained-tab-2-text": "۲. چگونه کار می‌کند", + "simplex-explained-tab-3-text": "۳. آنچه سرورها می‌بینند", + "simplex-explained-tab-1-p-1": "شما می‌توانید مخاطبین و گروه‌ها را ایجاد کنید و مانند هر پیام‌رسان دیگری، مکالمات دوطرفه داشته باشید.", + "simplex-explained-tab-1-p-2": "چگونه می‌تواند با صف‌های یک‌طرفه و بدون شناسه‌های پروفایل کاربر کار کند؟", + "simplex-explained-tab-2-p-1": "برای هر اتصال، شما از دو صف پیام جداگانه برای ارسال و دریافت پیام‌ها از طریق سرورهای مختلف استفاده می‌کنید.", + "simplex-explained-tab-2-p-2": "سرورها فقط پیام‌ها را به یک سمت منتقل می‌کنند و از جزئیات کامل مکالمات یا ارتباطات کاربران بی‌خبر هستند.", + "simplex-explained-tab-3-p-1": "سرورها برای هر صف دارای اعتبارنامه‌های جداگانه و ناشناس هستند و نمی‌دانند که این اعتبارنامه‌ها به کدام کاربران تعلق دارند.", + "simplex-explained-tab-3-p-2": "کاربران می‌توانند با استفاده از Tor برای دسترسی به سرورها، حریم خصوصی فراداده را بیشتر بهبود بخشند و از شناسایی بر اساس آدرس IP جلوگیری کنند.", + "chat-bot-example": "نمونه چت‌بات", + "smp-protocol": "پروتکل SMP", + "chat-protocol": "پروتکل چت", + "donate": "حمایت مالی", + "copyright-label": "© ۲۰۲۰-۲۰۲۵ SimpleX | پروژه متن‌باز", + "simplex-chat-protocol": "پروتکل چت SimpleX", + "terminal-cli": "رابط خط فرمان ترمینال", + "terms-and-privacy-policy": "سیاست حفظ حریم خصوصی", + "hero-header": "حریم خصوصی بازتعریف شده", + "hero-subheader": "اولین پیام‌رسان
بدون شناسه‌های کاربری", + "hero-p-1": "برنامه‌های دیگر دارای شناسه‌های کاربری هستند: Signal، Matrix، Session، Briar، Jami، Cwtch و غیره.
اما SimpleX این‌گونه نیست، حتی شماره‌های تصادفی هم ندارد.
این موضوع به طور اساسی حریم خصوصی شما را بهبود می‌بخشد.", + "hero-overlay-1-textlink": "چرا شناسه‌های کاربری برای حریم خصوصی بد هستند؟", + "hero-overlay-2-textlink": "SimpleX چگونه کار می‌کند؟", + "hero-overlay-3-textlink": "ارزیابی‌های امنیتی", + "hero-2-header": "یک ارتباط خصوصی برقرار کنید", + "hero-2-header-desc": "ویدئو نشان می‌دهد که چگونه می‌توانید از طریق کد QR یک‌بار مصرف دوست خود، به‌صورت حضوری یا از طریق یک لینک ویدیویی به او متصل شوید. همچنین می‌توانید با به اشتراک‌گذاری یک لینک دعوت، ارتباط برقرار کنید.", + "hero-overlay-1-title": "SimpleX چگونه کار می‌کند؟", + "hero-overlay-2-title": "چرا شناسه‌های کاربری برای حریم خصوصی بد هستند؟", + "hero-overlay-3-title": "ارزیابی‌های امنیتی", + "feature-1-title": "پیام‌های رمزنگاری شده انتها به انتها با ویرایش و فرمت‌بندی Markdown", + "feature-2-title": "تصاویر، ویدیوها و فایل‌های رمزنگاری شده انتها به انتها
", + "feature-3-title": "گروه‌های غیرمتمرکز رمزنگاری شده انتها به انتها — تنها کاربران می‌دانند که این گروه‎ها وجود دارند", + "feature-4-title": "پیام‌های صوتی رمزنگاری شده به صورت انتها به انتها", + "feature-5-title": "پیام‌های ناپدید شونده", + "feature-6-title": "تماس‌های صوتی و ویدیویی رمزنگاری شده انتها به انتها
", + "feature-7-title": "فضای ذخیره‌سازی رمزنگاری شده قابل حمل — انتقال پروفایل به دستگاه دیگر", + "feature-8-title": "حالت ناشناس —
منحصربه‌فرد برای SimpleX Chat", + "simplex-network-overlay-1-title": "مقایسه با پروتکل‌های پیام‌رسانی P2P", + "simplex-private-1-title": "دو لایه از
رمزنگاری انتها به انتها", + "simplex-private-2-title": "لایه اضافی از
رمزنگاری سرور", + "simplex-private-3-title": "انتقال TLS امن و تأییدشده
", + "simplex-private-4-title": "دسترسی اختیاری از طریق
Tor", + "simplex-private-5-title": "چندین لایه از
پدینگ محتوا", + "simplex-private-6-title": "مبادله کلید خارج از باند
", + "simplex-private-7-title": "تأیید صحت پیام
", + "simplex-private-8-title": "مخلوط‌سازی پیام
برای کاهش امکان رهگیری", + "simplex-private-9-title": "صف‌های پیام یک‌طرفه
", + "simplex-network-overlay-card-1-li-5": "تمام شبکه‌های P2P شناخته‌شده ممکن است در معرض حمله Sybil قرار بگیرند، زیرا هر گره قابل شناسایی است و شبکه به‌عنوان یک کل عمل می‌کند. تدابیر شناخته‌شده برای کاهش آن نیاز به یک مؤلفه متمرکز یا اثبات کار پرهزینه دارند. شبکه SimpleX هیچ کشف‌پذیری سروری ندارد، تکه‌تکه است و به‌عنوان چندین زیرشبکه ایزوله عمل می‌کند، که حملات در سطح شبکه را غیرممکن می‌سازد.", + "simplex-network-overlay-card-1-li-6": "شبکه‌های P2P ممکن است در معرض حمله DRDoS قرار بگیرند، زمانی که کلاینت‌ها می‌توانند ترافیک را دوباره پخش و تقویت کنند که منجر به عدم دسترسی به خدمات در سطح شبکه می‌شود. کلاینت‌های SimpleX تنها ترافیک را از اتصالات شناخته‌شده منتقل می‌کنند و نمی‌توانند توسط یک مهاجم برای تقویت ترافیک در کل شبکه استفاده شوند.", + "privacy-matters-overlay-card-1-p-1": "بسیاری از شرکت‌های بزرگ از اطلاعات مربوط به اینکه با چه کسانی ارتباط دارید، برای تخمین درآمد شما، فروش محصولات غیرضروری به شما و تعیین قیمت‌ها استفاده می‌کنند.", + "privacy-matters-overlay-card-1-p-2": "فروشگاه‌های آنلاین می‌دانند که افراد با درآمد پایین‌تر بیشتر احتمال دارد خریدهای فوری انجام دهند، بنابراین ممکن است قیمت‌های بالاتری تعیین کنند یا تخفیف‌ها را حذف کنند.", + "privacy-matters-overlay-card-1-p-3": "برخی از شرکت‌های مالی و بیمه از نمودارهای اجتماعی برای تعیین نرخ‌های بهره و حق بیمه‌ها استفاده می‌کنند. این اغلب باعث می‌شود که افراد با درآمد پایین‌تر بیشتر پرداخت کنند — که به آن 'حق بیمه فقر' گفته می‌شود.", + "privacy-matters-overlay-card-1-p-4": "شبکه SimpleX حریم خصوصی ارتباطات شما را بهتر از هر گزینه دیگری محافظت می‌کند و به‌طور کامل از در دسترس قرار گرفتن نمودار اجتماعی شما برای هر شرکت یا سازمانی جلوگیری می‌کند. حتی زمانی که افراد از سرورهای پیش‌پیکربندی‌شده در برنامه‌های چت SimpleX استفاده می‌کنند، اپراتورهای سرور از تعداد کاربران یا ارتباطات آن‌ها اطلاعی ندارند.", + "privacy-matters-overlay-card-2-p-1": "مدتی پیش، ما شاهد دستکاری در انتخابات بزرگ توسط یک شرکت مشاوره معتبر بودیم که از نمودارهای اجتماعی ما برای تحریف دیدگاه‌مان نسبت به دنیای واقعی و دستکاری در رأی‌گیری‌مان استفاده کرد.", + "privacy-matters-overlay-card-2-p-2": "برای اینکه عینی باشید و تصمیمات مستقل بگیرید، باید کنترل فضای اطلاعات خود را در دست داشته باشید. این تنها در صورتی ممکن است که از یک شبکه ارتباطی خصوصی استفاده کنید که به نمودار اجتماعی شما دسترسی نداشته باشد.", + "privacy-matters-overlay-card-2-p-3": "SimpleX اولین شبکه‌ای است که به‌طور ذاتی هیچ شناسه کاربری ندارد و به این ترتیب نمودار ارتباطات شما را بهتر از هر گزینه شناخته‌شده دیگری محافظت می‌کند.", + "privacy-matters-overlay-card-3-p-1": "همه باید به حریم خصوصی و امنیت ارتباطات خود اهمیت دهند — گفتگوهای بی‌ضرر می‌توانند شما را در خطر قرار دهند، حتی اگر چیزی برای پنهان کردن نداشته باشید.", + "privacy-matters-overlay-card-3-p-2": "یکی از شگفت‌انگیزترین داستان‌ها، تجربه محمدو ولد صلاحی است که در یادداشت‌هایش توصیف شده و در فیلم \"موریتانی\" به نمایش درآمده است. او بدون محاکمه به اردوگاه گوانتانامو منتقل شد و به مدت ۱۵ سال تحت شکنجه قرار گرفت. این اتفاق پس از یک تماس تلفنی با یکی از بستگانش در افغانستان رخ داد. او به ظن ارتباط با حملات ۱۱ سپتامبر، بدون محاکمه به اردوگاه گوانتانامو منتقل شد.", + "privacy-matters-overlay-card-3-p-3": "افراد عادی به خاطر آنچه که به‌صورت آنلاین به اشتراک می‌گذارند، حتی از طریق حساب‌های 'ناشناس' خود، حتی در کشورهای دموکراتیک دستگیر می‌شوند.", + "privacy-matters-overlay-card-3-p-4": "استفاده از یک پیام‌رسان رمزنگاری شده انتها به انتها کافی نیست؛ همه ما باید از پیام‌رسان‌هایی استفاده کنیم که حریم خصوصی شبکه‌های شخصی ما را محافظت می‌کنند — اینکه با چه کسانی ارتباط داریم.", + "simplex-unique-overlay-card-1-p-1": "برخلاف سایر شبکه‌های پیام‌رسان، SimpleX هیچ شناسه‌ای به کاربران اختصاص نمی‌دهد. این شبکه به شماره‌های تلفن، آدرس‌های مبتنی بر دامنه (مانند ایمیل یا XMPP)، نام‌های کاربری، کلیدهای عمومی یا حتی شماره‌های تصادفی برای شناسایی کاربران خود تکیه نمی‌کند — اپراتورهای سرور SimpleX نمی‌دانند چند نفر از سرورهای آن‌ها استفاده می‌کنند.", + "simplex-unique-overlay-card-1-p-2": "برای ارسال پیام‌ها، SimpleX از آدرس‌های ناشناس جفتی در صف‌های پیام یک‌طرفه استفاده می‌کند که برای پیام‌های دریافتی و ارسال‌شده جداگانه هستند و معمولاً از طریق سرورهای مختلف انجام می‌شود.", + "simplex-unique-overlay-card-1-p-3": "این طراحی از حریم خصوصی افرادی را که با آن‌ها ارتباط دارید، محافظت می‌کند و آن را از سرورهای شبکه SimpleX و هر ناظر دیگری پنهان می‌سازد. برای پنهان کردن آدرس IP خود از سرورها، می‌توانید از طریق Tor به سرورهای SimpleX متصل شوید.", + "simplex-unique-overlay-card-2-p-1": "از آنجایی که شما هیچ شناسه‌ای در شبکه SimpleX ندارید، هیچ‌کس نمی‌تواند با شما تماس بگیرد مگر اینکه یک آدرس کاربری یک‌بار مصرف یا موقتی را به‌عنوان کد QR یا لینک به اشتراک بگذارید.", + "simplex-unique-overlay-card-2-p-2": "حتی با وجود آدرس کاربری اختیاری که ممکن است برای ارسال درخواست‌های تماس اسپم استفاده شود، شما می‌توانید آن را تغییر دهید یا به‌طور کامل حذف کنید و در عین حال هیچ‌یک از ارتباطات خود را از دست ندهید.", + "simplex-unique-overlay-card-3-p-1": "چت SimpleX تمام داده‌های کاربری را تنها بر روی دستگاه‌های کلاینت ذخیره می‌کند و از یک فرمت پایگاه داده رمزنگاری شده قابل حمل استفاده می‌کند که می‌تواند اکسپورت و به هر دستگاه پشتیبانی‌شده‌ای منتقل شود.", + "simplex-unique-overlay-card-3-p-2": "پیام‌های رمزنگاری شده انتها به انتها به‌طور موقت بر روی سرورهای رله SimpleX نگهداری می‌شوند تا دریافت شوند و سپس به‌طور دائمی حذف می‌شوند.", + "simplex-unique-overlay-card-3-p-3": "برخلاف سرورهای شبکه‌های فدرال (ایمیل، XMPP یا Matrix)، سرورهای SimpleX حساب‌های کاربری را ذخیره نمی‌کنند و تنها پیام‌ها را منتقل می‌کنند، که حریم خصوصی هر دو طرف را محافظت می‌کند.", + "simplex-unique-overlay-card-3-p-4": "هیچ شناسه یا متن رمز مشترکی بین ترافیک سرور ارسال‌شده و دریافتی وجود ندارد — اگر کسی در حال نظارت باشد، نمی‌تواند به‌راحتی تشخیص دهد که چه کسی با چه کسی ارتباط برقرار می‌کند، حتی اگر TLS به خطر بیفتد.", + "simplex-unique-overlay-card-4-p-1": "شما می‌توانید از SimpleX با سرورهای خود استفاده کنید و همچنان با افرادی که از سرورهای پیش‌پیکربندی‌شده در برنامه‌ها استفاده می‌کنند، ارتباط برقرار کنید.", + "simplex-unique-overlay-card-4-p-2": "شبکه SimpleX از یک پروتکل باز استفاده می‌کند و SDK برای ایجاد ربات‌های چت ارائه می‌دهد، که امکان پیاده‌سازی خدماتی را فراهم می‌کند که کاربران می‌توانند از طریق برنامه‌های چت SimpleX با آن‌ها تعامل داشته باشند — ما واقعاً منتظر هستیم ببینیم چه خدماتی را با SimpleX ایجاد خواهید کرد.", + "simplex-unique-overlay-card-4-p-3": "اگر به توسعه برای شبکه SimpleX فکر می‌کنید، مثلاً ربات چت برای کاربران برنامه SimpleX یا ادغام کتابخانه چت SimpleX در برنامه‌های موبایل خود، لطفاً برای هرگونه مشاوره و پشتیبانی با ما تماس بگیرید.", + "simplex-unique-card-1-p-1": "SimpleX حریم خصوصی پروفایل، مخاطبین و فراداده‌های شما را محافظت می‌کند و آن‌ها را از سرورهای شبکه SimpleX و هر ناظر دیگری پنهان می‌سازد.", + "simplex-unique-card-1-p-2": "برخلاف هر شبکه پیام‌رسان دیگری، SimpleX هیچ شناسه‌ای به کاربران اختصاص نمی‌دهد — حتی شماره‌های تصادفی نیز وجود ندارد.", + "simplex-unique-card-2-p-1": "از آنجا که شما هیچ شناسه یا آدرس ثابتی در شبکه SimpleX ندارید، هیچ‌کس نمی‌تواند با شما تماس بگیرد مگر اینکه یک آدرس کاربری یک‌بار مصرف یا موقتی را به‌عنوان کد QR یا لینک به اشتراک بگذارید.", + "simplex-unique-card-3-p-1": "SimpleX تمام داده‌های کاربری را بر روی دستگاه‌های کلاینت در یک فرمت پایگاه داده رمزنگاری شده قابل حمل ذخیره می‌کند — این داده‌ها می‌توانند به دستگاه دیگری منتقل شوند.", + "simplex-unique-card-3-p-2": "پیام‌های رمزنگاری شده انتها به انتها به‌طور موقت بر روی سرورهای رله SimpleX نگهداری می‌شوند تا دریافت شوند و سپس به‌طور دائمی حذف می‌شوند.", + "simplex-unique-card-4-p-1": "شبکه SimpleX به‌طور کامل غیرمتمرکز و مستقل از هر ارز دیجیتال یا هر شبکه دیگری به جز اینترنت است.", + "simplex-unique-card-4-p-2": "شما می‌توانید از SimpleX با سرورهای خود استفاده کنید یا از سرورهای ارائه‌شده توسط ما استفاده کنید — و همچنان به هر کاربری متصل شوید.", + "join": "پیوستن", + "we-invite-you-to-join-the-conversation": "ما شما را به پیوستن به گفتگو دعوت می‌کنیم", + "join-the-REDDIT-community": "به جامعه REDDIT بپیوندید", + "join-us-on-GitHub": "به ما در GitHub بپیوندید", + "donate-here-to-help-us": "اینجا حمایت مالی کنید", + "sign-up-to-receive-our-updates": "برای دریافت به‌روزرسانی‌های ما ثبت‌نام کنید", + "enter-your-email-address": "آدرس ایمیل خود را وارد کنید", + "get-simplex": "برنامه دسکتاپ SimpleX را دریافت کنید", + "why-simplex-is-unique": "چرا SimpleX منحصربه‌فرد است", + "learn-more": "بیشتر بدانید", + "more-info": "اطلاعات بیشتر", + "hide-info": "پنهان کردن اطلاعات", + "contact-hero-header": "شما یک آدرس برای اتصال به چت SimpleX دریافت کرده‌اید", + "invitation-hero-header": "شما یک لینک یک‌بار مصرف برای اتصال به چت SimpleX دریافت کرده‌اید", + "contact-hero-subheader": "کد QR را با برنامه چت SimpleX روی گوشی یا تبلت خود اسکن کنید.", + "contact-hero-p-1": "کلیدهای عمومی و آدرس صف پیام در این لینک هنگام مشاهده این صفحه از طریق شبکه ارسال نمی‌شوند — آن‌ها در بخش هش لینک URL قرار دارند.", + "contact-hero-p-2": "هنوز برنامه چت SimpleX را دانلود نکرده‌اید؟", + "contact-hero-p-3": "از لینک‌های زیر برای دانلود برنامه استفاده کنید.", + "scan-qr-code-from-mobile-app": "کد QR را از برنامه موبایل اسکن کنید", + "to-make-a-connection": "برای برقراری ارتباط:", + "install-simplex-app": "برنامه SimpleX را نصب کنید", + "connect-in-app": "در برنامه متصل شوید", + "open-simplex-app": "برنامه SimpleX را باز کنید", + "tap-the-connect-button-in-the-app": "دکمه ‘اتصال’ را در برنامه بزنید", + "scan-the-qr-code-with-the-simplex-chat-app": "کد QR را با برنامه چت SimpleX اسکن کنید", + "scan-the-qr-code-with-the-simplex-chat-app-description": "کلیدهای عمومی و آدرس صف پیام در این لینک هنگام مشاهده این صفحه از طریق شبکه ارسال نمی‌شوند —
آن‌ها در بخش هش لینک URL قرار دارند.", + "installing-simplex-chat-to-terminal": "نصب چت SimpleX در ترمینال", + "use-this-command": "از این دستور استفاده کنید:", + "see-simplex-chat": "چت SimpleX را ببینید", + "github-repository": "مخزن GitHub", + "the-instructions--source-code": "برای دستورالعمل‌های دانلود یا کامپایل آن از کد منبع.", + "if-you-already-installed-simplex-chat-for-the-terminal": "اگر قبلاً چت SimpleX را برای ترمینال نصب کرده‌اید", + "if-you-already-installed": "اگر قبلاً نصب کرده‌اید", + "simplex-chat-for-the-terminal": "چت SimpleX برای ترمینال", + "copy-the-command-below-text": "دستور زیر را کپی کرده و در چت استفاده کنید:", + "privacy-matters-section-header": "چرا حریم خصوصی مهم است", + "privacy-matters-section-subheader": "حفظ حریم خصوصی فراداده‌های شما — با چه کسی صحبت می‌کنید — شما را در برابر موارد زیر محافظت می‌کند:", + "privacy-matters-section-label": "اطمینان حاصل کنید که پیام‌رسان شما به داده‌های شما دسترسی نداشته باشد!", + "simplex-private-section-header": "چه چیزی SimpleX را خصوصی می‌کند", + "tap-to-close": "برای بستن ضربه بزنید", + "simplex-network-section-header": "شبکه SimpleX", + "simplex-network-section-desc": "چت SimpleX بهترین حریم خصوصی را با ترکیب مزایای شبکه‌های P2P و شبکه‌های فدرال ارائه می‌دهد.", + "simplex-network-1-header": "برخلاف شبکه‌های P2P", + "simplex-network-1-desc": "تمام پیام‌ها از طریق سرورها ارسال می‌شوند که هم حریم خصوصی فراداده را بهتر تأمین می‌کند و هم تحویل پیام‌های غیرهمزمان قابل اعتماد را فراهم می‌آورد، در حالی که از بسیاری از مشکلات جلوگیری می‌کند.", + "simplex-network-1-overlay-linktext": "مشکلات شبکه‌های P2P", + "simplex-network-2-header": "برخلاف شبکه‌های فدرال", + "simplex-network-2-desc": "سرورهای رله SimpleX پروفایل‌های کاربری، مخاطبین و پیام‌های تحویل‌شده را ذخیره نمی‌کنند، به یکدیگر متصل نمی‌شوند و هیچ دایرکتوری سروری وجود ندارد.", + "simplex-network-3-header": "شبکه SimpleX", + "simplex-network-3-desc": "سرورها صف‌های یک‌طرفه را برای اتصال کاربران فراهم می‌کنند، اما هیچ دیدگاهی از نمودار ارتباطات شبکه ندارند — تنها کاربران این دید را دارند.", + "comparison-section-header": "مقایسه با سایر پروتکل‌ها", + "protocol-1-text": "سیگنال، پلتفرم‌های بزرگ", + "protocol-2-text": "XMPP، Matrix", + "protocol-3-text": "پروتکل‌های P2P", + "comparison-point-1-text": "نیاز به هویت جهانی دارد", + "comparison-point-2-text": "امکان حمله MITM", + "comparison-point-3-text": "وابستگی به DNS", + "comparison-point-4-text": "شبکه تک یا متمرکز", + "comparison-point-5-text": "مؤلفه مرکزی یا حمله‌ای در سطح شبکه", + "yes": "بله", + "no": "خیر", + "no-private": "خیر - خصوصی", + "no-secure": "خیر - امن", + "no-resilient": "خیر - مقاوم", + "no-decentralized": "خیر - غیرمتمرکز", + "no-federated": "خیر - فدرال", + "comparison-section-list-point-1": "معمولاً بر اساس شماره تلفن و در برخی موارد بر اساس نام‌های کاربری", + "comparison-section-list-point-2": "آدرس‌های مبتنی بر DNS", + "comparison-section-list-point-3": "کلید عمومی یا برخی دیگر از شناسه‌های منحصربه‌فرد جهانی", + "comparison-section-list-point-4a": "رله‌های SimpleX نمی‌توانند رمزنگاری انتها به انتها را به خطر بیندازند. توصیه می‌شود کد امنیتی را برای کاهش حمله در کانال out-of-band تأیید کنید", + "comparison-section-list-point-4": "اگر سرورهای اپراتور به خطر بیفتند. کد امنیتی را در سیگنال و برخی دیگر از برنامه‌ها تأیید کنید تا این خطر را کاهش دهید", + "comparison-section-list-point-5": "از حریم خصوصی فراداده‌های کاربران محافظت نمی‌کند", + "comparison-section-list-point-6": "در حالی که شبکه‌های P2P توزیع‌شده هستند، اما فدرال نیستند — آن‌ها به‌عنوان یک شبکه واحد عمل می‌کنند", + "comparison-section-list-point-7": "شبکه‌های P2P یا دارای یک نهاد مرکزی هستند یا کل شبکه می‌تواند به خطر بیفتد", + "see-here": "اینجا را ببینید", + "guide-dropdown-1": "شروع سریع", + "guide-dropdown-2": "ارسال پیام‌ها", + "guide-dropdown-3": "گروه‌های مخفی", + "guide-dropdown-4": "پروفایل‌های چت", + "guide-dropdown-5": "مدیریت داده‌ها", + "guide-dropdown-6": "تماس‌های صوتی و تصویری", + "guide-dropdown-7": "حریم خصوصی و امنیت", + "guide-dropdown-8": "تنظیمات برنامه", + "guide-dropdown-9": "برقراری ارتباطات", + "guide": "راهنما", + "docs-dropdown-1": "شبکه SimpleX", + "docs-dropdown-2": "دسترسی به فایل‌های اندروید", + "docs-dropdown-3": "دسترسی به پایگاه داده چت", + "docs-dropdown-4": "میزبانی سرور SMP", + "docs-dropdown-5": "میزبانی سرور XFTP", + "docs-dropdown-6": "سرورهای WebRTC", + "docs-dropdown-7": "SimpleX Chat را ترجمه کنید", + "docs-dropdown-8": "سرویس دایرکتوری SimpleX", + "docs-dropdown-9": "دانلودها", + "docs-dropdown-10": "شفافیت", + "docs-dropdown-11": "سؤالات متداول", + "docs-dropdown-12": "امنیت", + "docs-dropdown-14": "SimpleX برای کسب‌وکار", + "newer-version-of-eng-msg": "نسخه جدیدتری از این صفحه به زبان انگلیسی وجود دارد.", + "click-to-see": "کلیک کنید تا ببینید", + "menu": "منو", + "on-this-page": "در این صفحه", + "back-to-top": "بازگشت به بالا", + "glossary": "واژه‌نامه", + "simplex-chat-via-f-droid": "SimpleX Chat در F-Droid", + "simplex-chat-repo": "مخزن SimpleX Chat", + "stable-and-beta-versions-built-by-developers": "نسخه‌های پایدار و بتا که توسط توسعه‌دهندگان ساخته شده‌اند", + "f-droid-page-simplex-chat-repo-section-text": "برای افزودن آن به کلاینت F-Droid خود، کد QR را اسکن کنید یا از این URL استفاده کنید:", + "signing-key-fingerprint": "اثر انگشت کلید امضا (SHA-256)", + "f-droid-org-repo": "مخزن F-Droid.org", + "stable-versions-built-by-f-droid-org": "نسخه‌های پایدار که توسط F-Droid.org ساخته شده‌اند", + "releases-to-this-repo-are-done-1-2-days-later": "انتشارها در این مخزن چندین روز بعد انجام می‌شود", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat و مخازن F-Droid.org بیلدها را با کلیدهای متفاوتی امضا می‌کنند. برای تغییر، لطفاً پایگاه داده چت را اکسپورت کنید و برنامه را دوباره نصب کنید.", + "jobs": "پیوستن به تیم", + "please-enable-javascript": "لطفاً جاوا اسکریپت را فعال کنید تا کد QR را ببینید.", + "please-use-link-in-mobile-app": "لطفاً از لینک در برنامه موبایل استفاده کنید", + "simplex-private-10-title": "شناسه‌های موقت ناشناس دوطرفه", + "simplex-private-card-1-point-1": "پروتکل Double-ratchet — پیام‌رسانی OTR با محرمانگی کامل پیشرو و بازیابی در صورت نفوذ.", + "simplex-private-card-1-point-2": "NaCL cryptobox در هر صف برای جلوگیری از همبستگی ترافیک بین صف‌های پیام اگر TLS به خطر بیفتد.", + "simplex-private-card-2-point-1": "برای تحویل به گیرنده، لایه اضافی رمزنگاری سرور اضافه می‌شود تا در صورت به خطر افتادن TLS، از همبستگی بین ترافیک دریافتی و ارسالی سرور جلوگیری شود.", + "simplex-private-card-3-point-1": "فقط از TLS 1.2/1.3 با الگوریتم‌های قوی برای اتصالات کلاینت-سرور استفاده می‌شود.", + "simplex-private-card-3-point-2": "اثر انگشت سرور و اتصال کانال از حملات MITM و حملات replay جلوگیری می‌کنند.", + "simplex-private-card-3-point-3": "برای جلوگیری از حملات نشست، از سرگیری اتصال غیرفعال شده است.", + "simplex-private-card-4-point-1": "برای محافظت از آدرس IP خود، می‌توانید از طریق Tor یا برخی دیگر از شبکه‌های حمل و نقل پوششی به سرورها دسترسی پیدا کنید.", + "simplex-private-card-4-point-2": "برای استفاده از SimpleX از طریق Tor، لطفاً برنامه Orbot را نصب کرده و پروکسی SOCKS5 (یا VPN در iOS) را فعال کنید.", + "simplex-private-card-5-point-1": "SimpleX از padding محتوا برای هر لایه رمزنگاری استفاده می‌کند تا از حملات اندازه پیام جلوگیری کند.", + "simplex-private-card-5-point-2": "این کار باعث می‌شود که پیام‌های با اندازه‌های مختلف برای سرورها و ناظران شبکه یکسان به نظر برسند.", + "simplex-private-card-6-point-1": "بسیاری از شبکه‌های ارتباطی در برابر حملات MITM توسط سرورها یا ارائه‌دهندگان شبکه آسیب‌پذیر هستند.", + "simplex-private-card-6-point-2": "زمانی که آدرسی را به عنوان لینک یا کد QR به اشتراک می‌گذارید، برنامه‌های SimpleX کلیدهای یک‌بار مصرف را به صورت خارج از باند منتقل می‌کنند تا از این موضوع جلوگیری شود.", + "simplex-private-card-7-point-1": "برای تضمین یکپارچگی، پیام‌ها به صورت ترتیبی شماره‌گذاری شده و شامل هش پیام قبلی هستند.", + "simplex-private-card-7-point-2": "اگر هر پیامی اضافه، حذف یا تغییر یابد، گیرنده مطلع خواهد شد.", + "simplex-private-card-8-point-1": "سرورهای SimpleX به عنوان گره‌های میکس با تأخیر کم عمل می‌کنند — پیام‌های ورودی و خروجی ترتیب متفاوتی دارند.", + "simplex-private-card-9-point-1": "هر صف پیام، پیام‌ها را در یک جهت منتقل می‌کند و آدرس‌های ارسال و دریافت متفاوتی دارد.", + "simplex-private-card-9-point-2": "این کار نسبت به کارگزارهای پیام سنتی و فراداده‌های موجود، بردارهای حمله را کاهش می‌دهد.", + "simplex-private-card-10-point-1": "SimpleX از آدرس‌ها و اعتبارنامه‌های موقتی و ناشناس جفت‌به‌جفت برای هر مخاطب کاربر یا عضو گروه استفاده می‌کند.", + "simplex-private-card-10-point-2": "این امکان را فراهم می‌کند که پیام‌ها بدون شناسه‌های پروفایل کاربر ارسال شوند و حریم خصوصی فراداده بهتری نسبت به گزینه‌های دیگر ارائه می‌دهد.", + "privacy-matters-1-title": "تبلیغات و تبعیض قیمت", + "privacy-matters-1-overlay-1-title": "حریم خصوصی باعث صرفه‌جویی در هزینه شما می‌شود", + "privacy-matters-1-overlay-1-linkText": "حریم خصوصی باعث صرفه‌جویی در هزینه شما می‌شود", + "privacy-matters-2-title": "دستکاری در انتخابات", + "privacy-matters-2-overlay-1-title": "حریم خصوصی به شما قدرت می‌دهد", + "privacy-matters-2-overlay-1-linkText": "حریم خصوصی به شما قدرت می‌دهد", + "privacy-matters-3-title": "تعقیب قانونی به دلیل ارتباط بی‌گناه", + "privacy-matters-3-overlay-1-title": "حریم خصوصی از آزادی شما محافظت می‌کند", + "privacy-matters-3-overlay-1-linkText": "حریم خصوصی از آزادی شما محافظت می‌کند", + "simplex-unique-1-title": "شما حریم خصوصی کاملی دارید", + "simplex-unique-1-overlay-1-title": "حریم خصوصی کامل هویت، پروفایل، مخاطبین و فراداده‌های شما", + "simplex-unique-2-title": "شما از هرزنامه و سوءاستفاده محافظت می‌شوید", + "simplex-unique-2-overlay-1-title": "بهترین محافظت در برابر هرزنامه و سوءاستفاده", + "simplex-unique-3-title": "شما بر داده‌های خود کنترل دارید", + "simplex-unique-3-overlay-1-title": "مالکیت، کنترل و امنیت داده‌های شما", + "simplex-unique-4-title": "­شما مالک شبکه SimpleX هستید", + "simplex-unique-4-overlay-1-title": "کاملاً غیرمتمرکز — کاربران مالک شبکه SimpleX هستند", + "hero-overlay-card-1-p-1": "بسیاری از کاربران پرسیدند: اگر SimpleX هیچ شناسه کاربری ندارد، چگونه می‌تواند بداند پیام‌ها را کجا تحویل دهد؟", + "hero-overlay-card-1-p-2": "برای تحویل پیام‌ها، به جای شناسه‌های کاربری که توسط سایر شبکه‌ها استفاده می‌شود، SimpleX از شناسه‌های موقتی و ناشناس جفت‌به‌جفت صف‌های پیام استفاده می‌کند که برای هر یک از اتصالات شما جداگانه هستند — هیچ شناسه بلندمدتی وجود ندارد.", + "hero-overlay-card-1-p-3": "شما سرورهایی را که برای ارسال پیام‌ها به مخاطبین خود استفاده می‌کنید مشخص می‌کنید و همچنین از کدام سرور(ها) برای دریافت پیام‌ها بهره می‌برید. هر مکالمه احتمالاً از دو سرور مختلف استفاده می‌کند.", + "hero-overlay-card-1-p-4": "این طراحی از نشت هرگونه فراداده کاربران در سطح برنامه جلوگیری می‌کند. برای بهبود بیشتر حریم خصوصی و محافظت از آدرس IP خود، می‌توانید از طریق Tor به سرورهای پیام‌رسانی متصل شوید.", + "hero-overlay-card-1-p-5": "فقط دستگاه‌های کلاینت پروفایل‌های کاربری، مخاطبین و گروه‌ها را ذخیره می‌کنند؛ پیام‌ها با رمزنگاری انتها به انتهای دو لایه ارسال می‌شوند.", + "hero-overlay-card-1-p-6": "برای اطلاعات بیشتر به وایت پیپر SimpleX مراجعه کنید.", + "hero-overlay-card-2-p-1": "زمانی که کاربران هویت‌های پایدار دارند، حتی اگر این فقط یک شماره تصادفی مانند شناسه جلسه باشد، خطر این وجود دارد که ارائه‌دهنده یا یک مهاجم بتوانند مشاهده کنند که کاربران چگونه متصل شده‌اند و چند پیام ارسال می‌کنند.", + "hero-overlay-card-2-p-2": "سپس آن‌ها می‌توانند این اطلاعات را با شبکه‌های اجتماعی عمومی موجود همبسته کرده و برخی هویت‌های واقعی را شناسایی کنند.", + "hero-overlay-card-2-p-3": "حتی با خصوصی‌ترین برنامه‌ها که از خدمات Tor v3 استفاده می‌کنند، اگر شما با دو مخاطب مختلف از طریق یک پروفایل یکسان صحبت کنید، آن‌ها می‌توانند ثابت کنند که به یک شخص واحد متصل هستند.", + "hero-overlay-card-2-p-4": "SimpleX با عدم وجود شناسه‌های کاربری در طراحی خود، در برابر این حملات محافظت می‌کند. و اگر از حالت ناشناس استفاده کنید، برای هر مخاطب نام نمایشی متفاوتی خواهید داشت که از اشتراک‌گذاری هرگونه داده بین آن‌ها جلوگیری می‌کند.", + "hero-overlay-card-3-p-1": "Trail of Bits یک مشاوره پیشرو در زمینه امنیت و فناوری است که مشتریان آن شامل شرکت‌های بزرگ فناوری، نهادهای دولتی و پروژه‌های بزرگ بلاک‌چین می‌باشد.", + "hero-overlay-card-3-p-2": "Trail of Bits در نوامبر 2022 اجزای رمزنگاری و شبکه SimpleX را بررسی کرد. بیشتر بخوانید.", + "hero-overlay-card-3-p-3": "Trail of Bits در ژوئیه 2024 طراحی رمزنگاری پروتکل‌های شبکه SimpleX را بررسی کرد. بیشتر بخوانید.", + "simplex-network-overlay-card-1-p-1": "پروتکل‌ها و برنامه‌های پیام‌رسانی P2P مشکلات متعددی دارند که آن‌ها را نسبت به SimpleX کمتر قابل اعتماد، تحلیل آن‌ها را پیچیده‌تر و در برابر چندین نوع حمله آسیب‌پذیر می‌کند.", + "simplex-network-overlay-card-1-li-1": "شبکه‌های P2P به نوعی از DHT برای مسیریابی پیام‌ها وابسته هستند. طراحی‌های DHT باید بین تضمین تحویل و تأخیر تعادل برقرار کنند. SimpleX هم تضمین تحویل بهتری دارد و هم تأخیر کمتری نسبت به P2P، زیرا پیام می‌تواند به صورت اضافی از طریق چندین سرور به طور موازی منتقل شود و از سرورهای انتخاب‌شده توسط گیرنده استفاده کند. در شبکه‌های P2P، پیام به صورت ترتیبی از طریق O(log N) گره منتقل می‌شود و این گره‌ها توسط الگوریتم انتخاب می‌شوند.", + "simplex-network-overlay-card-1-li-2": "طراحی SimpleX، برخلاف اکثر شبکه‌های P2P، هیچ شناسه کاربری جهانی از هیچ نوعی، حتی موقتی، ندارد و فقط از شناسه‌های جفت‌به‌جفت موقتی استفاده می‌کند که این امر حریم خصوصی و محافظت از فراداده را بهتر فراهم می‌کند.", + "simplex-network-overlay-card-1-li-3": "P2P مشکل حملات MITM را حل نمی‌کند و بیشتر پیاده‌سازی‌های موجود از پیام‌های خارج از باند برای تبادل کلید اولیه استفاده نمی‌کنند. SimpleX از پیام‌های خارج از باند یا در برخی موارد، از اتصالات امن و مورد اعتماد پیشین برای تبادل کلید اولیه استفاده می‌کند.", + "simplex-network-overlay-card-1-li-4": "پیاده‌سازی‌های P2P می‌توانند توسط برخی از ارائه‌دهندگان اینترنت (مانند BitTorrent) مسدود شوند. SimpleX مستقل از نوع حمل و نقل است — این امکان را دارد که بر روی پروتکل‌های وب استاندارد، مانند WebSockets، کار کند." +} From ef60ceea12617ca5ebff0eb86e67e1ad5ba9a5da Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 9 Aug 2025 10:52:35 +0100 Subject: [PATCH 10/11] core, ui: markdown for hyperlinks, warn on unsanitized links, option to sanitize sent links (#6160) * core: markdown for "hidden" links * update, test * api docs * chatParseUri FFI function * ios: hyperlinks, offer to open sanitized links, an option to send sanitized links (enabled by default) * update markdown * android, desktop: ditto * ios: export localizations * core: rename constructor, change Maybe semantics for web links * rename --- .../Views/Chat/ChatItem/CILinkView.swift | 72 +++++- .../Views/Chat/ChatItem/MsgContentView.swift | 50 ++-- .../Chat/ComposeMessage/ComposeLinkView.swift | 4 +- .../Chat/ComposeMessage/ComposeView.swift | 94 +++++-- .../Shared/Views/ChatList/ChatListView.swift | 2 +- .../Views/NewChat/NewChatMenuButton.swift | 2 +- .../Views/UserSettings/DeveloperView.swift | 16 ++ .../Views/UserSettings/PrivacySettings.swift | 19 +- .../bg.xcloc/Localized Contents/bg.xliff | 18 +- .../cs.xcloc/Localized Contents/cs.xliff | 18 +- .../de.xcloc/Localized Contents/de.xliff | 18 +- .../en.xcloc/Localized Contents/en.xliff | 22 +- .../es.xcloc/Localized Contents/es.xliff | 18 +- .../fi.xcloc/Localized Contents/fi.xliff | 18 +- .../fr.xcloc/Localized Contents/fr.xliff | 18 +- .../hu.xcloc/Localized Contents/hu.xliff | 18 +- .../it.xcloc/Localized Contents/it.xliff | 18 +- .../ja.xcloc/Localized Contents/ja.xliff | 18 +- .../nl.xcloc/Localized Contents/nl.xliff | 18 +- .../pl.xcloc/Localized Contents/pl.xliff | 18 +- .../ru.xcloc/Localized Contents/ru.xliff | 18 +- .../th.xcloc/Localized Contents/th.xliff | 18 +- .../tr.xcloc/Localized Contents/tr.xliff | 18 +- .../uk.xcloc/Localized Contents/uk.xliff | 18 +- .../Localized Contents/zh-Hans.xliff | 18 +- apps/ios/SimpleX SE/ShareModel.swift | 15 +- apps/ios/SimpleX SE/ShareView.swift | 2 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 +- apps/ios/SimpleXChat/API.swift | 24 ++ apps/ios/SimpleXChat/AppGroup.swift | 6 + apps/ios/SimpleXChat/ChatTypes.swift | 20 +- apps/ios/SimpleXChat/ImageUtils.swift | 2 +- apps/ios/SimpleXChat/SimpleX.h | 1 + .../src/commonMain/cpp/android/simplex-api.c | 9 + .../src/commonMain/cpp/desktop/simplex-api.c | 9 + .../chat/simplex/common/model/ChatModel.kt | 28 +- .../chat/simplex/common/model/SimpleXAPI.kt | 20 +- .../chat/simplex/common/platform/Core.kt | 1 + .../simplex/common/views/chat/ComposeView.kt | 68 ++++- .../common/views/chat/item/TextItemView.kt | 240 +++++++++++++----- .../common/views/chatlist/ChatListView.kt | 2 +- .../common/views/chatlist/ChatPreviewView.kt | 34 +-- .../common/views/newchat/ConnectPlan.kt | 2 +- .../common/views/newchat/NewChatSheet.kt | 6 +- .../views/usersettings/DeveloperView.kt | 18 +- .../views/usersettings/PrivacySettings.kt | 42 +-- .../commonMain/resources/MR/base/strings.xml | 4 + bots/api/TYPES.md | 6 + flake.nix | 2 + libsimplex.dll.def | 1 + simplex-chat.cabal | 1 + src/Simplex/Chat/Markdown.hs | 66 ++++- src/Simplex/Chat/Mobile.hs | 34 ++- tests/MarkdownTests.hs | 33 ++- tests/MobileTests.hs | 11 + 55 files changed, 1004 insertions(+), 288 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift index 02cdad715e..b9bf6dd63a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -30,7 +30,7 @@ struct CILinkView: View { VStack(alignment: .leading, spacing: 6) { Text(linkPreview.title) .lineLimit(3) - Text(linkPreview.uri.absoluteString) + Text(linkPreview.uri) .font(.caption) .lineLimit(1) .foregroundColor(theme.colors.secondary) @@ -44,25 +44,71 @@ struct CILinkView: View { } } -func openBrowserAlert(uri: URL) { +func openBrowserAlert(uri: String) { + let (url, err) = sanitizeUri(uri) + if let url { + let uriStr = url.uri.absoluteString + showAlert( + NSLocalizedString("Open link?", comment: "alert title"), + message: uriStr.count > 160 ? "\(uriStr.prefix(160))…" : uriStr, + actions: { + if let sanitizedUri = url.sanitizedUri { + [ + cancelAlertAction, + UIAlertAction( + title: NSLocalizedString("Open full link", comment: "alert action"), + style: .destructive, + handler: { _ in UIApplication.shared.open(url.uri) } + ), + UIAlertAction( + title: NSLocalizedString("Open clean link", comment: "alert action"), + style: .default, + handler: { _ in UIApplication.shared.open(sanitizedUri) } + ) + ] + } else { + [ + cancelAlertAction, + UIAlertAction( + title: NSLocalizedString("Open", comment: "alert action"), + style: .default, + handler: { _ in UIApplication.shared.open(url.uri) } + ) + ] + } + } + ) + } else { + showInvalidLinkAlert(uri, error: err) + } +} + +func showInvalidLinkAlert(_ uri: String, error: String? = nil) { + let message = if let error, !error.isEmpty { + error + "\n" + uri + } else { + uri + } showAlert( - NSLocalizedString("Open link?", comment: "alert title"), - message: uri.absoluteString, - actions: {[ - cancelAlertAction, - UIAlertAction( - title: NSLocalizedString("Open", comment: "alert action"), - style: .default, - handler: { _ in UIApplication.shared.open(uri) } - ) - ]} + NSLocalizedString("Invalid link", comment: "alert title"), + message: message, + actions: {[okAlertAction]} ) } +func sanitizeUri(_ s: String) -> (url: (uri: URL, sanitizedUri: URL?)?, error: String?) { + let parsed = parseSanitizeUri(s) + return if let uri = URL(string: s), let uriInfo = parsed?.uriInfo { + (url: (uri: uri, sanitizedUri: uriInfo.sanitized.flatMap { URL(string: $0) }), error: nil) + } else { + (url: nil, error: parsed?.parseError) + } +} + struct LargeLinkPreview_Previews: PreviewProvider { static var previews: some View { let preview = LinkPreview( - uri: URL(string: "http://DuckDuckGo.com")!, + uri: "http://DuckDuckGo.com", title: "Privacy, simplified.", description: "", image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z" diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index e743e0bffa..2a1b526893 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -187,23 +187,25 @@ private func handleTextTaps( } } } - if let index, let (url, browser) = attributedStringLink(s, for: index) { + if let index, let (uri, browser) = attributedStringLink(s, for: index) { if browser { - openBrowserAlert(uri: url) - } else { + openBrowserAlert(uri: uri) + } else if let url = URL(string: uri) { UIApplication.shared.open(url) + } else { + showInvalidLinkAlert(uri) } } }) } - func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (URL, Bool)? { - var linkURL: URL? + func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (String, Bool)? { + var linkURL: String? var browser: Bool = false s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in if index >= range.location && index < range.location + range.length { - if let url = attrs[linkAttrKey] as? NSURL { - linkURL = url.absoluteURL + if let url = attrs[linkAttrKey] as? String { + linkURL = url browser = attrs[webLinkAttrKey] != nil } else if let showSecrets, let i = attrs[secretAttrKey] as? Int { if showSecrets.wrappedValue.contains(i) { @@ -356,22 +358,32 @@ func messageText( case .uri: attrs = linkAttrs() if !preview { - let s = t.lowercased() - let link = s.hasPrefix("http://") || s.hasPrefix("https://") + let link = t.hasPrefix("http://") || t.hasPrefix("https://") ? t : "https://" + t - attrs[linkAttrKey] = NSURL(string: link) + attrs[linkAttrKey] = link attrs[webLinkAttrKey] = true handleTaps = true } - case let .simplexLink(linkType, simplexUri, smpHosts): + case let .hyperLink(text, uri): attrs = linkAttrs() + if let text { t = text } if !preview { - attrs[linkAttrKey] = NSURL(string: simplexUri) + attrs[linkAttrKey] = uri + attrs[webLinkAttrKey] = true handleTaps = true } - if case .description = privacySimplexLinkModeDefault.get() { - t = simplexLinkText(linkType, smpHosts) + case let .simplexLink(text, linkType, simplexUri, smpHosts): + attrs = linkAttrs() + if !preview { + attrs[linkAttrKey] = simplexUri + handleTaps = true + } + if let s = text ?? (privacySimplexLinkModeDefault.get() == .description ? linkType.description : nil) { + res.append(NSAttributedString(string: s + " ", attributes: attrs)) + italic = italic ?? UIFont(descriptor: descr.withSymbolicTraits(.traitItalic) ?? descr, size: descr.pointSize) + attrs[.font] = italic + t = viaHost(smpHosts) } case let .command(cmdStr): snippet = snippet ?? UIFont.monospacedSystemFont(ofSize: descr.pointSize, weight: .regular) @@ -403,13 +415,13 @@ func messageText( case .email: attrs = linkAttrs() if !preview { - attrs[linkAttrKey] = NSURL(string: "mailto:" + ft.text) + attrs[linkAttrKey] = "mailto:" + ft.text handleTaps = true } case .phone: attrs = linkAttrs() if !preview { - attrs[linkAttrKey] = NSURL(string: "tel:" + t.replacingOccurrences(of: " ", with: "")) + attrs[linkAttrKey] = "tel:" + t.replacingOccurrences(of: " ", with: "") handleTaps = true } case .unknown: () @@ -439,7 +451,11 @@ private func mentionText(_ name: String) -> String { } func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String { - linkType.description + " " + "(via \(smpHosts.first ?? "?"))" + linkType.description + " " + viaHost(smpHosts) +} + +func viaHost(_ smpHosts: [String]) -> String { + "(via \(smpHosts.first ?? "?"))" } struct MsgContentView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift index e629a984df..878ebd9cbf 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift @@ -49,7 +49,7 @@ struct ComposeLinkView: View { VStack(alignment: .center, spacing: 4) { Text(linkPreview.title) .lineLimit(1) - Text(linkPreview.uri.absoluteString) + Text(linkPreview.uri) .font(.caption) .lineLimit(1) .foregroundColor(theme.colors.secondary) @@ -63,7 +63,7 @@ struct ComposeLinkView: View { struct SmallLinkPreview_Previews: PreviewProvider { static var previews: some View { let preview = LinkPreview( - uri: URL(string: "http://DuckDuckGo.com")!, + uri: "http://DuckDuckGo.com", title: "Privacy, simplified.", description: "", image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z" diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index a6e6518e7b..619f0b95f3 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -329,10 +329,10 @@ struct ComposeView: View { @Binding var selectedRange: NSRange var disabledText: LocalizedStringKey? = nil - @State var linkUrl: URL? = nil + @State var linkUrl: String? = nil @State var hasSimplexLink: Bool = false - @State var prevLinkUrl: URL? = nil - @State var pendingLinkUrl: URL? = nil + @State var prevLinkUrl: String? = nil + @State var pendingLinkUrl: String? = nil @State var cancelledLinks: Set = [] @Environment(\.colorScheme) private var colorScheme @@ -353,6 +353,8 @@ struct ComposeView: View { @UserDefault(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false + @AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = true + @State private var updatingCompose = false var body: some View { VStack(spacing: 0) { @@ -454,8 +456,26 @@ struct ComposeView: View { .ignoresSafeArea(.all, edges: .bottom) } .onChange(of: composeState.message) { msg in - let parsedMsg = parseSimpleXMarkdown(msg) - composeState = composeState.copy(parsedMessage: parsedMsg ?? FormattedText.plain(msg)) + if updatingCompose { + updatingCompose = false + return + } + var parsedMsg = parseSimpleXMarkdown(msg) + if privacySanitizeLinks, let parsed = parsedMsg { + let r = sanitizeMessage(parsed) + if let sanitizedPos = r.sanitizedPos { + updatingCompose = true + composeState = composeState.copy(message: r.message, parsedMessage: r.parsedMsg) + if sanitizedPos < selectedRange.location { + selectedRange = NSRange(location: sanitizedPos, length: 0) + } + parsedMsg = r.parsedMsg + } else { + composeState = composeState.copy(parsedMessage: parsedMsg) + } + } else { + composeState = composeState.copy(parsedMessage: parsedMsg ?? FormattedText.plain(msg)) + } if composeState.linkPreviewAllowed { if msg.count > 0 { showLinkPreview(parsedMsg) @@ -464,7 +484,7 @@ struct ComposeView: View { hasSimplexLink = false } } else if msg.count > 0 && !chat.groupFeatureEnabled(.simplexLinks) { - (_, hasSimplexLink) = getSimplexLink(parsedMsg) + (_, hasSimplexLink) = getMessageLinks(parsedMsg) } else { hasSimplexLink = false } @@ -845,7 +865,7 @@ struct ComposeView: View { switch (composeState.preview) { case let .linkPreview(linkPreview: linkPreview): if let parsedMsg = parseSimpleXMarkdown(msgText), - let url = getSimplexLink(parsedMsg).url, + let url = getMessageLinks(parsedMsg).url, let linkPreview = linkPreview, url == linkPreview.uri { return .link(text: msgText, preview: linkPreview) @@ -1448,7 +1468,7 @@ struct ComposeView: View { private func showLinkPreview(_ parsedMsg: [FormattedText]?) { prevLinkUrl = linkUrl - (linkUrl, hasSimplexLink) = getSimplexLink(parsedMsg) + (linkUrl, hasSimplexLink) = getMessageLinks(parsedMsg) if let url = linkUrl { if url != composeState.linkPreview?.uri && url != pendingLinkUrl { pendingLinkUrl = url @@ -1465,39 +1485,38 @@ struct ComposeView: View { } } - private func getSimplexLink(_ parsedMsg: [FormattedText]?) -> (url: URL?, hasSimplexLink: Bool) { + private func getMessageLinks(_ parsedMsg: [FormattedText]?) -> (url: String?, hasSimplexLink: Bool) { guard let parsedMsg else { return (nil, false) } - let url: URL? = if let uri = parsedMsg.first(where: { ft in - ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) - }) { - URL(string: uri.text) - } else { - nil - } let simplexLink = parsedMsgHasSimplexLink(parsedMsg) - return (url, simplexLink) + for ft in parsedMsg { + if let link = ft.linkUri, !cancelledLinks.contains(link) && !isSimplexLink(link) { + return (link, simplexLink) + } + } + return (nil, simplexLink) } private func isSimplexLink(_ link: String) -> Bool { - link.starts(with: "https://simplex.chat") || link.starts(with: "http://simplex.chat") + link.starts(with: "https://simplex.chat") || link.starts(with: "http://simplex.chat") || link.starts(with: "simplex:/") } private func cancelLinkPreview() { - if let pendingLink = pendingLinkUrl?.absoluteString { + if let pendingLink = pendingLinkUrl { cancelledLinks.insert(pendingLink) } - if let uri = composeState.linkPreview?.uri.absoluteString { + if let uri = composeState.linkPreview?.uri { cancelledLinks.insert(uri) } pendingLinkUrl = nil composeState = composeState.copy(preview: .noPreview) } - private func loadLinkPreview(_ url: URL) { - if pendingLinkUrl == url { + private func loadLinkPreview(_ urlStr: String) { + if pendingLinkUrl == urlStr, let url = URL(string: urlStr) { composeState = composeState.copy(preview: .linkPreview(linkPreview: nil)) getLinkPreview(url: url) { linkPreview in - if let linkPreview, pendingLinkUrl == url { + if let linkPreview, pendingLinkUrl == urlStr { + privacyLinkPreviewsShowAlertGroupDefault.set(false) // to avoid showing alert to current users, show alert in v6.5 composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview)) } else { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -1516,3 +1535,32 @@ struct ComposeView: View { cancelledLinks = [] } } + +func sanitizeMessage(_ parsedMsg: [FormattedText]) -> (message: String, parsedMsg: [FormattedText], sanitizedPos: Int?) { + var pos: Int = 0 + var updatedMsg = "" + var sanitizedPos: Int? = nil + let updatedParsedMsg = parsedMsg.map { ft in + var updated = ft + switch ft.format { + case .uri: + if let sanitized = parseSanitizeUri(ft.text)?.uriInfo?.sanitized { + updated = FormattedText(text: sanitized, format: .uri) + pos += updated.text.count + sanitizedPos = pos + } + case let .hyperLink(text, uri): + if let sanitized = parseSanitizeUri(uri)?.uriInfo?.sanitized { + let updatedText = if let text { "[\(text)](\(sanitized))" } else { sanitized } + updated = FormattedText(text: updatedText, format: .hyperLink(showText: text, linkUri: sanitized)) + pos += updated.text.count + sanitizedPos = pos + } + default: + pos += ft.text.count + } + updatedMsg += updated.text + return updated + } + return (message: updatedMsg, parsedMsg: updatedParsedMsg, sanitizedPos: sanitizedPos) +} diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 9f453f1aa3..0450bd439c 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -629,7 +629,7 @@ struct ChatListSearchBar: View { } else { if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue searchFocussed = false - if case let .simplexLink(linkType, _, smpHosts) = link.format { + if case let .simplexLink(_, linkType, _, smpHosts) = link.format { ignoreSearchTextChange = true searchText = simplexLinkText(linkType, smpHosts) } diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index c009bf0d35..2e3119a8b8 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -381,7 +381,7 @@ struct ContactsListSearchBar: View { } else { if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue searchFocussed = false - if case let .simplexLink(linkType, _, smpHosts) = link.format { + if case let .simplexLink(_, linkType, _, smpHosts) = link.format { ignoreSearchTextChange = true searchText = simplexLinkText(linkType, smpHosts) } diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift index 54454b7cef..6df2d5422e 100644 --- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -14,6 +14,7 @@ struct DeveloperView: View { @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false @State private var hintsUnchanged = hintDefaultsUnchanged() + @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @Environment(\.colorScheme) var colorScheme @@ -65,6 +66,21 @@ struct DeveloperView: View { Text("Developer options") } } + Section("Deprecated options") { + settingsRow("link", color: theme.colors.secondary) { + Picker("SimpleX links", selection: $simplexLinkMode) { + ForEach( + SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode]) + ) { mode in + Text(mode.text) + } + } + } + .frame(height: 36) + .onChange(of: simplexLinkMode) { mode in + privacySimplexLinkModeDefault.set(mode) + } + } } } } diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 06fe20a3fd..48c2efe0ac 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -14,11 +14,11 @@ struct PrivacySettings: View { @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true @AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true + @AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = true @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true @AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true - @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @@ -75,8 +75,12 @@ struct PrivacySettings: View { Toggle("Send link previews", isOn: $useLinkPreviews) .onChange(of: useLinkPreviews) { linkPreviews in privacyLinkPreviewsGroupDefault.set(linkPreviews) + privacyLinkPreviewsShowAlertGroupDefault.set(false) // to avoid showing alert to current users, show alert in v6.5 } } + settingsRow("link", color: theme.colors.secondary) { + Toggle("Remove link tracking", isOn: $privacySanitizeLinks) + } settingsRow("message", color: theme.colors.secondary) { Toggle("Show last messages", isOn: $showChatPreviews) } @@ -89,19 +93,6 @@ struct PrivacySettings: View { m.draftChatId = nil } } - settingsRow("link", color: theme.colors.secondary) { - Picker("SimpleX links", selection: $simplexLinkMode) { - ForEach( - SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode]) - ) { mode in - Text(mode.text) - } - } - } - .frame(height: 36) - .onChange(of: simplexLinkMode) { mode in - privacySimplexLinkModeDefault.set(mode) - } } header: { Text("Chats") .foregroundColor(theme.colors.secondary) diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index bfa410ca8e..00a8ea67ae 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -2563,6 +2563,10 @@ swipe action Потвърждениe за доставка! No comment provided by engineer.
+ + Deprecated options + No comment provided by engineer. + Description Описание @@ -4289,7 +4293,7 @@ More improvements are coming soon! Invalid link Невалиден линк - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5460,10 +5464,18 @@ Requires compatible VPN. Отвори конзолата authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group Отвори група @@ -6193,6 +6205,10 @@ swipe action Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Острани член diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 87524b09ab..1f32d7010e 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -2455,6 +2455,10 @@ swipe action Potvrzení o doručení! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Popis @@ -4128,7 +4132,7 @@ More improvements are coming soon! Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5267,10 +5271,18 @@ Vyžaduje povolení sítě VPN. Otevřete konzolu chatu authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group new chat action @@ -5976,6 +5988,10 @@ swipe action Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Odstranit člena diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index f42e5d20ce..6f294d82d5 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -2680,6 +2680,10 @@ swipe action Empfangsbestätigungen! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Beschreibung @@ -4517,7 +4521,7 @@ Weitere Verbesserungen sind bald verfügbar! Invalid link Ungültiger Link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5770,11 +5774,19 @@ Dies erfordert die Aktivierung eines VPNs. Chat-Konsole öffnen authentication reason + + Open clean link + alert action + Open conditions Nutzungsbedingungen öffnen No comment provided by engineer. + + Open full link + alert action + Open group Gruppe öffnen @@ -6564,6 +6576,10 @@ swipe action Bild entfernen No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Mitglied entfernen diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 05b9b9bc9d..7a6efcfba8 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -2684,6 +2684,11 @@ swipe action Delivery receipts! No comment provided by engineer. + + Deprecated options + Deprecated options + No comment provided by engineer. + Description Description @@ -4522,7 +4527,7 @@ More improvements are coming soon! Invalid link Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5777,11 +5782,21 @@ Requires compatible VPN. Open chat console authentication reason + + Open clean link + Open clean link + alert action + Open conditions Open conditions No comment provided by engineer. + + Open full link + Open full link + alert action + Open group Open group @@ -6572,6 +6587,11 @@ swipe action Remove image No comment provided by engineer. + + Remove link tracking + Remove link tracking + No comment provided by engineer. + Remove member Remove member diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 7f33515577..71415bb02d 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -2680,6 +2680,10 @@ swipe action ¡Confirmación de entrega! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Descripción @@ -4517,7 +4521,7 @@ More improvements are coming soon! Invalid link Enlace no válido - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5770,11 +5774,19 @@ Requiere activación de la VPN. Abrir consola de Chat authentication reason + + Open clean link + alert action + Open conditions Abrir condiciones No comment provided by engineer. + + Open full link + alert action + Open group Grupo abierto @@ -6564,6 +6576,10 @@ swipe action Eliminar imagen No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Expulsar miembro diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 4b42238a22..de08d5f39e 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -2426,6 +2426,10 @@ swipe action Toimituskuittaukset! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Kuvaus @@ -4096,7 +4100,7 @@ More improvements are coming soon! Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5233,10 +5237,18 @@ Edellyttää VPN:n sallimista. Avaa keskustelukonsoli authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group new chat action @@ -5942,6 +5954,10 @@ swipe action Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Poista jäsen diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 36c1fdfc91..bfc09b29ec 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -2661,6 +2661,10 @@ swipe action Justificatifs de réception ! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Description @@ -4474,7 +4478,7 @@ D'autres améliorations sont à venir ! Invalid link Lien invalide - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5694,11 +5698,19 @@ Nécessite l'activation d'un VPN. Ouvrir la console du chat authentication reason + + Open clean link + alert action + Open conditions Ouvrir les conditions No comment provided by engineer. + + Open full link + alert action + Open group Ouvrir le groupe @@ -6467,6 +6479,10 @@ swipe action Enlever l'image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Retirer le membre diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index af02421c01..a23021b67e 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -2680,6 +2680,10 @@ swipe action Kézbesítési jelentések! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Leírás @@ -4517,7 +4521,7 @@ További fejlesztések hamarosan! Invalid link Érvénytelen hivatkozás - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5770,11 +5774,19 @@ VPN engedélyezése szükséges. Csevegési konzol megnyitása authentication reason + + Open clean link + alert action + Open conditions Feltételek megnyitása No comment provided by engineer. + + Open full link + alert action + Open group Csoport megnyitása @@ -6564,6 +6576,10 @@ swipe action Kép eltávolítása No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Eltávolítás diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index a54056dc4d..7f789cb85c 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -2680,6 +2680,10 @@ swipe action Ricevute di consegna! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Descrizione @@ -4517,7 +4521,7 @@ Altri miglioramenti sono in arrivo! Invalid link Link non valido - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5770,11 +5774,19 @@ Richiede l'attivazione della VPN. Apri la console della chat authentication reason + + Open clean link + alert action + Open conditions Apri le condizioni No comment provided by engineer. + + Open full link + alert action + Open group Apri gruppo @@ -6564,6 +6576,10 @@ swipe action Rimuovi immagine No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Rimuovi membro diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 44db30b201..2c34ae3499 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -2504,6 +2504,10 @@ swipe action 配信通知! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description 説明 @@ -4177,7 +4181,7 @@ More improvements are coming soon! Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5318,10 +5322,18 @@ VPN を有効にする必要があります。 チャットのコンソールを開く authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group new chat action @@ -6027,6 +6039,10 @@ swipe action Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member メンバーを除名する diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 495fd3ee1a..4079a6cd9c 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -2670,6 +2670,10 @@ swipe action Ontvangstbewijzen! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Beschrijving @@ -4498,7 +4502,7 @@ Binnenkort meer verbeteringen! Invalid link Ongeldige link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5743,11 +5747,19 @@ Vereist het inschakelen van VPN. Chat console openen authentication reason + + Open clean link + alert action + Open conditions Open voorwaarden No comment provided by engineer. + + Open full link + alert action + Open group Open groep @@ -6530,6 +6542,10 @@ swipe action Verwijder afbeelding No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Lid verwijderen diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 224bc41e42..a0a929ac63 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -2630,6 +2630,10 @@ swipe action Potwierdzenia dostawy! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Opis @@ -4406,7 +4410,7 @@ More improvements are coming soon! Invalid link Nieprawidłowy link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5607,10 +5611,18 @@ Wymaga włączenia VPN. Otwórz konsolę czatu authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group Grupa otwarta @@ -6373,6 +6385,10 @@ swipe action Usuń obraz No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Usuń członka diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 00f91be200..d4f37a9b35 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -2680,6 +2680,10 @@ swipe action Отчёты о доставке! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Описание @@ -4516,7 +4520,7 @@ More improvements are coming soon! Invalid link Ошибка ссылки - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5769,11 +5773,19 @@ Requires compatible VPN. Открыть консоль authentication reason + + Open clean link + alert action + Open conditions Открыть условия No comment provided by engineer. + + Open full link + alert action + Open group Открыть группу @@ -6563,6 +6575,10 @@ swipe action Удалить изображение No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Удалить члена группы diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 2c514afc72..d2f4b979d8 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -2414,6 +2414,10 @@ swipe action ใบตอบรับการจัดส่ง! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description คำอธิบาย @@ -4080,7 +4084,7 @@ More improvements are coming soon! Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5212,10 +5216,18 @@ Requires compatible VPN. เปิดคอนโซลการแชท authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group new chat action @@ -5919,6 +5931,10 @@ swipe action Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member ลบสมาชิกออก diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 3e94c4acd2..0e758dc1d7 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -2680,6 +2680,10 @@ swipe action Mesaj gönderildi bilgisi! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Açıklama @@ -4516,7 +4520,7 @@ Daha fazla iyileştirme yakında geliyor! Invalid link Geçersiz bağlantı - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5767,11 +5771,19 @@ VPN'nin etkinleştirilmesi gerekir. Sohbet konsolunu aç authentication reason + + Open clean link + alert action + Open conditions Açık koşullar No comment provided by engineer. + + Open full link + alert action + Open group Grubu aç @@ -6561,6 +6573,10 @@ swipe action Resmi kaldır No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Kişiyi sil diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index d87f1cc613..555baafd69 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -2679,6 +2679,10 @@ swipe action Квитанції про доставку! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Опис @@ -4515,7 +4519,7 @@ More improvements are coming soon! Invalid link Невірне посилання - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5767,11 +5771,19 @@ Requires compatible VPN. Відкрийте консоль чату authentication reason + + Open clean link + alert action + Open conditions Відкриті умови No comment provided by engineer. + + Open full link + alert action + Open group Відкрита група @@ -6561,6 +6573,10 @@ swipe action Видалити зображення No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Видалити учасника diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index f8767b14e4..562b623217 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -2662,6 +2662,10 @@ swipe action 送达回执! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description 描述 @@ -4487,7 +4491,7 @@ More improvements are coming soon! Invalid link 无效链接 - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5728,11 +5732,19 @@ Requires compatible VPN. 打开聊天控制台 authentication reason + + Open clean link + alert action + Open conditions 打开条款 No comment provided by engineer. + + Open full link + alert action + Open group 打开群 @@ -6497,6 +6509,10 @@ swipe action 移除图片 No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member 删除成员 diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift index 12a775f85c..5080cf2040 100644 --- a/apps/ios/SimpleX SE/ShareModel.swift +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -390,7 +390,7 @@ enum SharedContent { switch self { case let .image(preview, _): .image(text: comment, image: preview) case let .movie(preview, duration, _): .video(text: comment, image: preview, duration: duration) - case let .url(preview): .link(text: preview.uri.absoluteString + (comment == "" ? "" : "\n" + comment), preview: preview) + case let .url(preview): .link(text: preview.uri + (comment == "" ? "" : "\n" + comment), preview: preview) case .text: .text(comment) case .data: .file(comment) } @@ -464,12 +464,13 @@ fileprivate func getSharedContent(_ ip: NSItemProvider) async -> Result ParsedUri? { + var c = s.cString(using: .utf8)! + if let cjson = chat_parse_uri(&c) { + if let d = dataFromCString(cjson) { + do { + return try jsonDecoder.decode(ParsedUri.self, from: d) + } catch { + logger.error("parseSanitizeUri jsonDecoder.decode error: \(error.localizedDescription)") + } + } + } + return nil +} + +public struct ParsedUri: Decodable { + public var uriInfo: UriInfo? + public var parseError: String +} + +public struct UriInfo: Decodable { + public var scheme: String + public var sanitized: String? +} + @inline(__always) public func fromCString(_ c: UnsafeMutablePointer) -> String { let s = String.init(cString: c) diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 5eed01a2a2..47932397fc 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -27,6 +27,8 @@ let GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED = "appLocalAuthEnabled" public let GROUP_DEFAULT_ALLOW_SHARE_EXTENSION = "allowShareExtension" // replaces DEFAULT_PRIVACY_LINK_PREVIEWS let GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" +public let GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT = "privacyLinkPreviewsShowAlert" +public let GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS = "privacySanitizeLinks" // This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used @@ -95,6 +97,8 @@ public func registerGroupDefaults() { GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED: true, GROUP_DEFAULT_ALLOW_SHARE_EXTENSION: false, GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS: true, + GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT: true, + GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS: true, GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true, GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false, GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true, @@ -222,6 +226,8 @@ public let allowShareExtensionGroupDefault = BoolDefault(defaults: groupDefaults public let privacyLinkPreviewsGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS) +public let privacyLinkPreviewsShowAlertGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT) + // This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index f868b787ee..7a70c6b664 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -4611,6 +4611,11 @@ public struct FormattedText: Decodable, Hashable { public var text: String public var format: Format? + public init(text: String, format: Format? = nil) { + self.text = text + self.format = format + } + public static func plain(_ text: String) -> [FormattedText] { text.isEmpty ? [] @@ -4620,6 +4625,14 @@ public struct FormattedText: Decodable, Hashable { public var isSecret: Bool { if case .secret = format { true } else { false } } + + public var linkUri: String? { + switch format { + case .uri: text + case let .hyperLink(_, linkUri): linkUri + default: nil + } + } } public enum Format: Decodable, Equatable, Hashable { @@ -4630,7 +4643,8 @@ public enum Format: Decodable, Equatable, Hashable { case secret case colored(color: FormatColor) case uri - case simplexLink(linkType: SimplexLinkType, simplexUri: String, smpHosts: [String]) + case hyperLink(showText: String?, linkUri: String) + case simplexLink(showText: String?, linkType: SimplexLinkType, simplexUri: String, smpHosts: [String]) case command(commandStr: String) case mention(memberName: String) case email @@ -4748,14 +4762,14 @@ extension ReportReason: Decodable { // Struct to use with simplex API public struct LinkPreview: Codable, Equatable, Hashable { - public init(uri: URL, title: String, description: String = "", image: String) { + public init(uri: String, title: String, description: String = "", image: String) { self.uri = uri self.title = title self.description = description self.image = image } - public var uri: URL + public var uri: String public var title: String // TODO remove once optional in haskell public var description: String = "" diff --git a/apps/ios/SimpleXChat/ImageUtils.swift b/apps/ios/SimpleXChat/ImageUtils.swift index be43158bc1..c70ca5edd8 100644 --- a/apps/ios/SimpleXChat/ImageUtils.swift +++ b/apps/ios/SimpleXChat/ImageUtils.swift @@ -446,7 +446,7 @@ public func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { let resized = resizeImageToStrSizeSync(image, maxDataSize: 14000), let title = metadata.title, let uri = metadata.originalURL { - linkPreview = LinkPreview(uri: uri, title: title, image: resized) + linkPreview = LinkPreview(uri: uri.absoluteString, title: title, image: resized) } } cb(linkPreview) diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index 8a443017e1..66f570f1b6 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -24,6 +24,7 @@ extern char *chat_send_cmd_retry(chat_ctrl ctl, char *cmd, int retryNum); extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait); extern char *chat_parse_markdown(char *str); extern char *chat_parse_server(char *str); +extern char *chat_parse_uri(char *str); extern char *chat_password_hash(char *pwd, char *salt); extern char *chat_valid_name(char *name); extern int chat_json_length(char *str); diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index d8fa2c65a7..cfbed65c76 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -64,6 +64,7 @@ extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); +extern char *chat_parse_uri(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_valid_name(const char *name); extern int chat_json_length(const char *str); @@ -146,6 +147,14 @@ Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, __unused j return res; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatParseUri(JNIEnv *env, __unused jclass clazz, jstring str) { + const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); + jstring res = (*env)->NewStringUTF(env, chat_parse_uri(_str)); + (*env)->ReleaseStringUTFChars(env, str, _str); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, __unused jclass clazz, jstring pwd, jstring salt) { const char *_pwd = (*env)->GetStringUTFChars(env, pwd, JNI_FALSE); diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index 2614b1a561..076e323ca6 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -37,6 +37,7 @@ extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); +extern char *chat_parse_uri(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_valid_name(const char *name); extern int chat_json_length(const char *str); @@ -156,6 +157,14 @@ Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, jclass cla return res; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatParseUri(JNIEnv *env, jclass clazz, jstring str) { + const char *_str = encode_to_utf8_chars(env, str); + jstring res = decode_to_utf8_string(env, chat_parse_uri(_str)); + (*env)->ReleaseStringUTFChars(env, str, _str); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, jclass clazz, jstring pwd, jstring salt) { const char *_pwd = encode_to_utf8_chars(env, pwd); diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 962bd82dd3..1d00d7cdb0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -4369,19 +4369,12 @@ sealed class MsgChatLink { @Serializable class FormattedText(val text: String, val format: Format? = null) { - fun link(mode: SimplexLinkMode): String? = when (format) { - is Format.Uri -> if (text.startsWith("http://", ignoreCase = true) || text.startsWith("https://", ignoreCase = true)) text else "https://$text" - is Format.SimplexLink -> if (mode == SimplexLinkMode.BROWSER) text else format.simplexUri - is Format.Email -> "mailto:$text" - is Format.Phone -> "tel:$text" - else -> null - } - - fun viewText(mode: SimplexLinkMode): String = - if (format is Format.SimplexLink && mode == SimplexLinkMode.DESCRIPTION) simplexLinkText(format.linkType, format.smpHosts) else text - - fun simplexLinkText(linkType: SimplexLinkType, smpHosts: List): String = - "${linkType.description} (${String.format(generalGetString(MR.strings.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})" + val linkUri: String? get() = + when (format) { + is Format.Uri -> text + is Format.HyperLink -> format.linkUri + else -> null + } companion object { fun plain(text: String): List = if (text.isEmpty()) emptyList() else listOf(FormattedText(text)) @@ -4397,7 +4390,13 @@ sealed class Format { @Serializable @SerialName("secret") class Secret: Format() @Serializable @SerialName("colored") class Colored(val color: FormatColor): Format() @Serializable @SerialName("uri") class Uri: Format() - @Serializable @SerialName("simplexLink") class SimplexLink(val linkType: SimplexLinkType, val simplexUri: String, val smpHosts: List): Format() + @Serializable @SerialName("hyperLink") class HyperLink(val showText: String?, val linkUri: String): Format() + @Serializable @SerialName("simplexLink") class SimplexLink(val showText: String?, val linkType: SimplexLinkType, val simplexUri: String, val smpHosts: List): Format() { + val simplexLinkText: String get() = + "${linkType.description} $viaHosts" + val viaHosts: String get() = + "(${String.format(generalGetString(MR.strings.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})" + } @Serializable @SerialName("command") class Command(val commandStr: String): Format() @Serializable @SerialName("mention") class Mention(val memberName: String): Format() @Serializable @SerialName("email") class Email: Format() @@ -4412,6 +4411,7 @@ sealed class Format { is Secret -> SpanStyle(color = Color.Transparent, background = SecretColor) is Colored -> SpanStyle(color = this.color.uiColor) is Uri -> linkStyle + is HyperLink -> linkStyle is SimplexLink -> linkStyle is Command -> SpanStyle(color = MaterialTheme.colors.primary, fontFamily = FontFamily.Monospace) is Mention -> SpanStyle(fontWeight = FontWeight.Medium) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index b23869849d..3e56ef3a2e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -104,6 +104,9 @@ class AppPreferences { val privacyProtectScreen = mkBoolPreference(SHARED_PREFS_PRIVACY_PROTECT_SCREEN, true) val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true) val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true) + val privacyLinkPreviewsShowAlert = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS_SHOW_ALERT, true) + val privacySanitizeLinks = mkBoolPreference(SHARED_PREFS_PRIVACY_SANITIZE_LINKS, true) + // TODO remove val privacyChatListOpenLinks = mkEnumPreference(SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS, PrivacyChatListOpenLinksMode.ASK) { PrivacyChatListOpenLinksMode.values().firstOrNull { it.name == this } } val simplexLinkMode: SharedPreference = mkSafeEnumPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default) val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true) @@ -369,7 +372,9 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages" private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline" private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews" - private const val SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS = "ChatListOpenLinks" + private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS_SHOW_ALERT = "PrivacyLinkPreviewsShowAlert" + private const val SHARED_PREFS_PRIVACY_SANITIZE_LINKS = "PrivacySanitizeLinks" + private const val SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS = "ChatListOpenLinks" // TODO remove private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode" private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews" private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" @@ -4629,6 +4634,19 @@ data class ParsedServerAddress ( var parseError: String ) +fun parseSanitizeUri(s: String): ParsedUri? { + val parsed = chatParseUri(s) + return runCatching { json.decodeFromString(ParsedUri.serializer(), parsed) } + .onFailure { Log.d(TAG, "parseSanitizeUri decode error: $it") } + .getOrNull() +} + +@Serializable +data class ParsedUri(val uriInfo: UriInfo?, val parseError: String) + +@Serializable +data class UriInfo(val scheme: String, val sanitized: String?) + @Serializable data class NetCfg( val socksProxy: String?, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 9d8a699775..35194ba1e6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -28,6 +28,7 @@ external fun chatRecvMsg(ctrl: ChatCtrl): String external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String external fun chatParseMarkdown(str: String): String external fun chatParseServer(str: String): String +external fun chatParseUri(str: String): String external fun chatPasswordHash(pwd: String, salt: String): String external fun chatValidName(name: String): String external fun chatJsonLength(str: String): Int diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 5f99ac77d6..fdc6a3fed5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -356,16 +356,21 @@ fun ComposeView( fun isSimplexLink(link: String): Boolean = link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true) - fun getSimplexLink(parsedMsg: List?): Pair { + fun getMessageLinks(parsedMsg: List?): Pair { if (parsedMsg == null) return null to false - val link = parsedMsg.firstOrNull { ft -> ft.format is Format.Uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) } val simplexLink = parsedMsg.any { ft -> ft.format is Format.SimplexLink } - return link?.text to simplexLink + for (ft in parsedMsg) { + val link = ft.linkUri + if (link != null && !cancelledLinks.contains(link) && !isSimplexLink(link)) { + return link to simplexLink + } + } + return null to simplexLink } val linkUrl = rememberSaveable { mutableStateOf(null) } // default value parsed because of draft - val hasSimplexLink = rememberSaveable { mutableStateOf(getSimplexLink(parseToMarkdown(composeState.value.message.text)).second) } + val hasSimplexLink = rememberSaveable { mutableStateOf(getMessageLinks(parseToMarkdown(composeState.value.message.text)).second) } val prevLinkUrl = rememberSaveable { mutableStateOf(null) } val pendingLinkUrl = rememberSaveable { mutableStateOf(null) } val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() @@ -382,6 +387,7 @@ fun ComposeView( if (wait != null) delay(wait) val lp = getLinkPreview(url) if (lp != null && pendingLinkUrl.value == url) { + chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.set(false) // to avoid showing alert to current users, show alert in v6.5 composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(lp)) pendingLinkUrl.value = null } else if (pendingLinkUrl.value == url) { @@ -394,7 +400,7 @@ fun ComposeView( fun showLinkPreview(parsedMessage: List?) { prevLinkUrl.value = linkUrl.value - val linkParsed = getSimplexLink(parsedMessage) + val linkParsed = getMessageLinks(parsedMessage) linkUrl.value = linkParsed.first hasSimplexLink.value = linkParsed.second val url = linkUrl.value @@ -501,7 +507,7 @@ fun ComposeView( return when (val composePreview = composeState.value.preview) { is ComposePreview.CLinkPreview -> { val parsedMsg = parseToMarkdown(msgText) - val url = getSimplexLink(parsedMsg).first + val url = getMessageLinks(parsedMsg).first val lp = composePreview.linkPreview if (lp != null && url == lp.uri) { MsgContent.MCLink(msgText, preview = lp) @@ -861,9 +867,53 @@ fun ComposeView( } } + fun sanitizeMessage(parsedMsg: List): Triple, Int?> { + var pos = 0 + var updatedMsg = "" + var sanitizedPos: Int? = null + val updatedParsedMsg = parsedMsg.map { ft -> + var updated = ft + when(ft.format) { + is Format.Uri -> { + val sanitized = parseSanitizeUri(ft.text)?.uriInfo?.sanitized + if (sanitized != null) { + updated = FormattedText(text = sanitized, format = Format.Uri()) + pos += updated.text.count() + sanitizedPos = pos + } + } + is Format.HyperLink -> { + val sanitized = parseSanitizeUri(ft.format.linkUri)?.uriInfo?.sanitized + if (sanitized != null) { + val updatedText = if (ft.format.showText == null) sanitized else "[${ft.format.showText}]($sanitized)" + updated = FormattedText(text = updatedText, format = Format.HyperLink(showText = ft.format.showText, linkUri = sanitized)) + pos += updated.text.count() + sanitizedPos = pos + } + } + else -> + pos += ft.text.count() + } + updatedMsg += updated.text + updated + } + return Triple(updatedMsg, updatedParsedMsg, sanitizedPos) + } + fun onMessageChange(s: ComposeMessage) { - val parsedMessage = parseToMarkdown(s.text) - composeState.value = composeState.value.copy(message = s, parsedMessage = parsedMessage ?: FormattedText.plain(s.text)) + var parsedMessage = parseToMarkdown(s.text) + if (chatModel.controller.appPrefs.privacySanitizeLinks.state.value && parsedMessage != null) { + val (updatedMsg, updatedParsedMsg, sanitizedPos) = sanitizeMessage(parsedMessage) + if (sanitizedPos == null) { + composeState.value = composeState.value.copy(message = s, parsedMessage = parsedMessage) + } else { + val message = if (sanitizedPos < s.selection.start) s.copy(text = updatedMsg) else ComposeMessage(updatedMsg, TextRange(sanitizedPos, sanitizedPos)) + composeState.value = composeState.value.copy(message = message, parsedMessage = updatedParsedMsg) + parsedMessage = updatedParsedMsg + } + } else { + composeState.value = composeState.value.copy(message = s, parsedMessage = parsedMessage ?: FormattedText.plain(s.text)) + } if (isShortEmoji(s.text)) { textStyle.value = if (s.text.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont } else { @@ -876,7 +926,7 @@ fun ComposeView( hasSimplexLink.value = false } } else if (s.text.isNotEmpty() && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)) { - hasSimplexLink.value = getSimplexLink(parsedMessage).second + hasSimplexLink.value = getMessageLinks(parsedMessage).second } else { hasSimplexLink.value = false } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index 291eedb4ee..6a6485bc42 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -1,5 +1,8 @@ package chat.simplex.common.views.chat.item +import SectionItemView +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material.MaterialTheme @@ -12,15 +15,15 @@ import androidx.compose.ui.input.pointer.* import androidx.compose.ui.platform.* import androidx.compose.ui.text.* import androidx.compose.ui.text.AnnotatedString.Range -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.font.* +import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.helpers.* +import chat.simplex.res.* import kotlinx.coroutines.* val reserveTimestampStyle = SpanStyle(color = Color.Transparent) @@ -145,55 +148,102 @@ fun MarkdownText ( if (prefix != null) append(prefix) for ((i, ft) in formattedText.withIndex()) { if (ft.format == null) append(ft.text) - else if (toggleSecrets && ft.format is Format.Secret) { - val ftStyle = ft.format.style - hasSecrets = true - val key = i.toString() - withAnnotation(tag = "SECRET", annotation = key) { - if (showSecrets[key] == true) append(ft.text) else withStyle(ftStyle) { append(ft.text) } - } - } else if (ft.format is Format.Mention) { - val mention = mentions?.get(ft.format.memberName) - - if (mention != null) { - if (mention.memberRef != null) { - val displayName = mention.memberRef.displayName - val name = if (mention.memberRef.localAlias.isNullOrEmpty()) { - displayName - } else { - "${mention.memberRef.localAlias} ($displayName)" + else when(ft.format) { + is Format.Bold -> withStyle(ft.format.style) { append(ft.text) } + is Format.Italic -> withStyle(ft.format.style) { append(ft.text) } + is Format.StrikeThrough -> withStyle(ft.format.style) { append(ft.text) } + is Format.Snippet -> withStyle(ft.format.style) { append(ft.text) } + is Format.Colored -> withStyle(ft.format.style) { append(ft.text) } + is Format.Secret -> { + val ftStyle = ft.format.style + if (toggleSecrets) { + hasSecrets = true + val key = i.toString() + withAnnotation(tag = "SECRET", annotation = key) { + if (showSecrets[key] == true) append(ft.text) else withStyle(ftStyle) { append(ft.text) } } - val mentionStyle = if (mention.memberId == userMemberId) ft.format.style.copy(color = MaterialTheme.colors.primary) else ft.format.style - - withStyle(mentionStyle) { append(mentionText(name)) } } else { - withStyle( ft.format.style) { append(mentionText(ft.format.memberName)) } - } - } else { - append(ft.text) - } - } else if (ft.format is Format.Command) { - if (sendCommandMsg == null) { - append(ft.text) - } else { - hasCommands = true - val ftStyle = ft.format.style - val cmd = ft.format.commandStr - withAnnotation(tag = "COMMAND", annotation = cmd) { - withStyle(ftStyle) { append("/$cmd") } + withStyle(ftStyle) { append(ft.text) } } } - } else { - val link = ft.link(linkMode) - if (link != null) { + is Format.Mention -> { + val mention = mentions?.get(ft.format.memberName) + if (mention != null) { + val ftStyle = ft.format.style + if (mention.memberRef != null) { + val displayName = mention.memberRef.displayName + val name = if (mention.memberRef.localAlias.isNullOrEmpty()) { + displayName + } else { + "${mention.memberRef.localAlias} ($displayName)" + } + val mentionStyle = if (mention.memberId == userMemberId) ftStyle.copy(color = MaterialTheme.colors.primary) else ftStyle + withStyle(mentionStyle) { append(mentionText(name)) } + } else { + withStyle(ftStyle) { append(mentionText(ft.format.memberName)) } + } + } else { + append(ft.text) + } + } + is Format.Command -> + if (sendCommandMsg == null) { + append(ft.text) + } else { + hasCommands = true + val ftStyle = ft.format.style + val cmd = ft.format.commandStr + withAnnotation(tag = "COMMAND", annotation = cmd) { + withStyle(ftStyle) { append("/$cmd") } + } + } + is Format.Uri -> { hasLinks = true - val ftStyle = ft.format.style - withAnnotation(tag = if (ft.format is Format.SimplexLink) "SIMPLEX_URL" else "URL", annotation = link) { - withStyle(ftStyle) { append(ft.viewText(linkMode)) } + val ftStyle = Format.linkStyle + val s = ft.text + val link = if (s.startsWith("http://") || s.startsWith("https://")) s else "https://$s" + withAnnotation(tag = "WEB_URL", annotation = link) { + withStyle(ftStyle) { append(ft.text) } } - } else { - withStyle(ft.format.style) { append(ft.text) } } + is Format.HyperLink -> { + hasLinks = true + val ftStyle = Format.linkStyle + withAnnotation(tag = "WEB_URL", annotation = ft.format.linkUri) { + withStyle(ftStyle) { append(ft.format.showText ?: ft.text) } + } + } + is Format.SimplexLink -> { + hasLinks = true + val ftStyle = Format.linkStyle + val link = + if (linkMode == SimplexLinkMode.BROWSER && ft.format.showText == null && !ft.text.startsWith("[")) ft.text + else ft.format.simplexUri + val t = ft.format.showText ?: if (linkMode == SimplexLinkMode.DESCRIPTION) ft.format.linkType.description else null + withAnnotation(tag = "SIMPLEX_URL", annotation = link) { + if (t == null) { + withStyle(ftStyle) { append(ft.text) } + } else { + withStyle(ftStyle) { append("$t ") } + withStyle(ftStyle.copy(fontStyle = FontStyle.Italic)) { append(ft.format.viaHosts) } + } + } + } + is Format.Email -> { + hasLinks = true + val ftStyle = Format.linkStyle + withAnnotation(tag = "OTHER_URL", annotation = "mailto:${ft.text}") { + withStyle(ftStyle) { append(ft.text) } + } + } + is Format.Phone -> { + hasLinks = true + val ftStyle = Format.linkStyle + withAnnotation(tag = "OTHER_URL", annotation = "tel:${ft.text}") { + withStyle(ftStyle) { append(ft.text) } + } + } + is Format.Unknown -> append(ft.text) } } if (meta?.isLive == true) { @@ -209,10 +259,12 @@ fun MarkdownText ( ClickableText(annotatedText, style = style, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow, onLongClick = { offset -> if (hasLinks) { - annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) } - annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) } + val withAnnotation: (String, (Range) -> Unit) -> Unit = { tag, f -> + annotatedText.getStringAnnotations(tag, start = offset, end = offset).firstOrNull()?.let(f) + } + withAnnotation("WEB_URL") { a -> onLinkLongClick(a.item) } + withAnnotation("SIMPLEX_URL") { a -> onLinkLongClick(a.item) } + withAnnotation("OTHER_URL") { a -> onLinkLongClick(a.item) } } }, onClick = { offset -> @@ -220,37 +272,33 @@ fun MarkdownText ( annotatedText.getStringAnnotations(tag, start = offset, end = offset).firstOrNull()?.let(f) } if (hasLinks && uriHandler != null) { - withAnnotation("URL") { a -> - try { - uriHandler.openUri(a.item) - } catch (e: Exception) { - // It can happen, for example, when you click on a text 0.00001 but don't have any app that can catch - // `tel:` scheme in url installed on a device (no phone app or contacts, maybe) - Log.e(TAG, "Open url: ${e.stackTraceToString()}") - } - } + withAnnotation("WEB_URL") { a -> openBrowserAlert(a.item, uriHandler) } + withAnnotation("OTHER_URL") { a -> safeOpenUri(a.item, uriHandler) } withAnnotation("SIMPLEX_URL") { a -> uriHandler.openVerifiedSimplexUri(a.item) } - } else if (hasSecrets) { + } + if (hasSecrets) { withAnnotation("SECRET") { a -> val key = a.item showSecrets[key] = !(showSecrets[key] ?: false) } - } else if (hasCommands && sendCommandMsg != null) { + } + if (hasCommands && sendCommandMsg != null) { withAnnotation("COMMAND") { a -> sendCommandMsg("/${a.item}") } } }, onHover = { offset -> val hasAnnotation: (String) -> Boolean = { tag -> annotatedText.hasStringAnnotations(tag, start = offset, end = offset) } icon.value = - if (hasAnnotation("URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) { + if (hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) { PointerIcon.Hand } else { PointerIcon.Default } }, shouldConsumeEvent = { offset -> - annotatedText.hasStringAnnotations(tag = "URL", start = offset, end = offset) + annotatedText.hasStringAnnotations(tag = "WEB_URL", start = offset, end = offset) || annotatedText.hasStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) + || annotatedText.hasStringAnnotations(tag = "OTHER_URL", start = offset, end = offset) } ) } else { @@ -319,6 +367,74 @@ fun ClickableText( ) } +fun openBrowserAlert(uri: String, uriHandler: UriHandler) { + val (res, err) = sanitizeUri(uri) + if (res == null) { + showInvalidLinkAlert(uri, err) + } else { + val message = if (uri.count() > 160) uri.substring(0, 159) + "…" else uri + val sanitizedUri = res.second + if (sanitizedUri == null) { + AlertManager.shared.showAlertDialog( + generalGetString(MR.strings.privacy_chat_list_open_web_link_question), + message, + confirmText = generalGetString(MR.strings.open_verb), + onConfirm = { safeOpenUri(uri, uriHandler) } + ) + } else { + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.privacy_chat_list_open_web_link_question), + message, + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + safeOpenUri(uri, uriHandler) + }) { + Text(generalGetString(MR.strings.privacy_chat_list_open_full_web_link), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + SectionItemView({ + AlertManager.shared.hideAlert() + safeOpenUri(sanitizedUri, uriHandler) + }) { + Text(generalGetString(MR.strings.privacy_chat_list_open_clean_web_link), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + }) + } + } +} + +fun safeOpenUri(uri: String, uriHandler: UriHandler) { + try { + uriHandler.openUri(uri) + } catch (e: Exception) { + // It can happen, for example, when you click on a text 0.00001 but don't have any app that can catch + // `tel:` scheme in url installed on a device (no phone app or contacts, maybe) + Log.e(TAG, "Open url: ${e.stackTraceToString()}") + showInvalidLinkAlert(uri, error = e.message) + } +} + +fun showInvalidLinkAlert(uri: String, error: String? = null) { + val message = if (error.isNullOrEmpty()) { uri } else { error + "\n" + uri } + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_parsing_uri_title), message) +} + +fun sanitizeUri(s: String): Pair?, String?> { + val parsed = parseSanitizeUri(s) + return if (parsed?.uriInfo != null) { + (true to parsed.uriInfo.sanitized) to null + } else { + null to parsed?.parseError + } +} + private fun isRtl(s: CharSequence): Boolean { for (element in s) { val d = Character.getDirectionality(element) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 5e9bac515d..7b5be0c323 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -659,7 +659,7 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState // if SimpleX link is pasted, show connection dialogue hideKeyboard(view) if (link.format is Format.SimplexLink) { - val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts) + val linkText = link.format.simplexLinkText searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) } searchShowingSimplexLink.value = true diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 35681ff1d2..d5d6facafe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -298,37 +298,9 @@ fun ChatPreviewView( val uriHandler = LocalUriHandler.current when (mc) { is MsgContent.MCLink -> SmallContentPreview { - val linkClicksEnabled = remember { appPrefs.privacyChatListOpenLinks.state }.value != PrivacyChatListOpenLinksMode.NO - IconButton({ - when (appPrefs.privacyChatListOpenLinks.get()) { - PrivacyChatListOpenLinksMode.YES -> uriHandler.openUriCatching(mc.preview.uri) - PrivacyChatListOpenLinksMode.NO -> defaultClickAction() - PrivacyChatListOpenLinksMode.ASK -> AlertManager.shared.showAlertDialogButtonsColumn( - title = generalGetString(MR.strings.privacy_chat_list_open_web_link_question), - text = mc.preview.uri, - buttons = { - Column { - if (chatModel.chatId.value != chat.id) { - SectionItemView({ - AlertManager.shared.hideAlert() - defaultClickAction() - }) { - Text(stringResource(MR.strings.open_chat), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - SectionItemView({ - AlertManager.shared.hideAlert() - uriHandler.openUriCatching(mc.preview.uri) - } - ) { - Text(stringResource(MR.strings.privacy_chat_list_open_web_link), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - } - ) - } - }, - if (linkClicksEnabled) Modifier.desktopPointerHoverIconHand() else Modifier, + IconButton( + { openBrowserAlert(mc.preview.uri, uriHandler) }, + Modifier.desktopPointerHoverIconHand(), ) { Image(base64ToBitmap(mc.preview.image), null, contentScale = ContentScale.Crop) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index b1c5caedc9..434cb6ce27 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -63,7 +63,7 @@ private suspend fun planAndConnectTask( val (connectionLink, connectionPlan) = result val link = strHasSingleSimplexLink(shortOrFullLink.trim()) val linkText = if (link?.format is Format.SimplexLink) - "

${link.simplexLinkText(link.format.linkType, link.format.smpHosts)}" + "

${link.format.simplexLinkText}" else "" when (connectionPlan) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 1cb1903a14..ef6e426141 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -519,10 +519,8 @@ private fun ContactsSearchBar( // if SimpleX link is pasted, show connection dialogue hideKeyboard(view) if (link.format is Format.SimplexLink) { - val linkText = - link.simplexLinkText(link.format.linkType, link.format.smpHosts) - searchText.value = - searchText.value.copy(linkText, selection = TextRange.Zero) + val linkText = link.format.simplexLinkText + searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) } searchShowingSimplexLink.value = true searchChatFilteredBySimplexLink.value = null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index c5a4ae5f70..dcb71a552d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -2,18 +2,10 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced -import SectionSpacer import SectionTextFooter import SectionView -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler -import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import dev.icerock.moko.resources.compose.painterResource @@ -67,7 +59,15 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) -> SettingsPreferenceItem(painterResource(MR.images.ic_avg_pace), stringResource(MR.strings.show_slow_api_calls), appPreferences.showSlowApiCalls) } } - SectionBottomSpacer() + SectionDividerSpaced(maxTopPadding = true) + SectionView(stringResource(MR.strings.deprecated_options_section).uppercase()) { + val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode + SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = { + simplexLinkMode.set(it) + chatModel.simplexLinkMode.value = it + }) + SectionBottomSpacer() + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 915119fa64..2771b5ac62 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -56,16 +56,22 @@ fun PrivacySettingsView( setPerformLA: (Boolean) -> Unit ) { ColumnWithScrollBar { - val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode AppBarTitle(stringResource(MR.strings.your_privacy)) PrivacyDeviceSection(showSettingsModal, setPerformLA) SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_chats)) { - SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) - ChatListLinksOptions(appPrefs.privacyChatListOpenLinks.state, onSelected = { - appPrefs.privacyChatListOpenLinks.set(it) - }) + SettingsPreferenceItem( + painterResource(MR.images.ic_travel_explore), + stringResource(MR.strings.send_link_previews), + chatModel.controller.appPrefs.privacyLinkPreviews, + onChange = { _ -> chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.set(false) } // to avoid showing alert to current users, show alert in v6.5 + ) + SettingsPreferenceItem( + painterResource(MR.images.ic_link), + stringResource(MR.strings.sanitize_links_toggle), + chatModel.controller.appPrefs.privacySanitizeLinks + ) SettingsPreferenceItem( painterResource(MR.images.ic_chat_bubble), stringResource(MR.strings.privacy_show_last_messages), @@ -84,10 +90,6 @@ fun PrivacySettingsView( chatModel.draftChatId.value = null } }) - SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = { - simplexLinkMode.set(it) - chatModel.simplexLinkMode.value = it - }) } SectionDividerSpaced() @@ -218,27 +220,7 @@ fun PrivacySettingsView( } @Composable -private fun ChatListLinksOptions(state: State, onSelected: (PrivacyChatListOpenLinksMode) -> Unit) { - val values = remember { - PrivacyChatListOpenLinksMode.entries.map { - when (it) { - PrivacyChatListOpenLinksMode.YES -> it to generalGetString(MR.strings.privacy_chat_list_open_links_yes) - PrivacyChatListOpenLinksMode.NO -> it to generalGetString(MR.strings.privacy_chat_list_open_links_no) - PrivacyChatListOpenLinksMode.ASK -> it to generalGetString(MR.strings.privacy_chat_list_open_links_ask) - } - } - } - ExposedDropDownSettingRow( - generalGetString(MR.strings.privacy_chat_list_open_links), - values, - state, - icon = painterResource(MR.images.ic_open_in_new), - onSelected = onSelected - ) -} - -@Composable -private fun SimpleXLinkOptions(simplexLinkModeState: State, onSelected: (SimplexLinkMode) -> Unit) { +fun SimpleXLinkOptions(simplexLinkModeState: State, onSelected: (SimplexLinkMode) -> Unit) { val modeValues = listOf(SimplexLinkMode.DESCRIPTION, SimplexLinkMode.FULL) val pickerValues = modeValues + if (modeValues.contains(simplexLinkModeState.value)) emptyList() else listOf(simplexLinkModeState.value) val values = remember { diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 22cdd3a643..0b53b8d577 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1072,6 +1072,7 @@ Enable logs Database IDs and Transport isolation option. Developer options + Deprecated options Show internal errors Show slow API calls Shutdown? @@ -1368,6 +1369,7 @@ The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled). Without Tor or VPN, your IP address will be visible to file servers. Send link previews + Remove link tracking Show last messages Message draft App data backup @@ -1434,6 +1436,8 @@ Ask Open web link? Open link + Open full link + Open clean link YOU diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 416fd1ca3a..74e761107f 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -1979,8 +1979,14 @@ Colored: Uri: - type: "uri" +HyperLink: +- type: "hyperLink" +- showText: string? +- linkUri: string + SimplexLink: - type: "simplexLink" +- showText: string? - linkType: [SimplexLinkType](#simplexlinktype) - simplexUri: string - smpHosts: [string] diff --git a/flake.nix b/flake.nix index a5c4eb2a27..1520fb2ee3 100644 --- a/flake.nix +++ b/flake.nix @@ -382,6 +382,7 @@ "chat_migrate_init" "chat_parse_markdown" "chat_parse_server" + "chat_parse_uri" "chat_password_hash" "chat_read_file" "chat_recv_msg" @@ -489,6 +490,7 @@ "chat_migrate_init" "chat_parse_markdown" "chat_parse_server" + "chat_parse_uri" "chat_password_hash" "chat_read_file" "chat_recv_msg" diff --git a/libsimplex.dll.def b/libsimplex.dll.def index 2945d52e83..76e6f9f3ee 100644 --- a/libsimplex.dll.def +++ b/libsimplex.dll.def @@ -12,6 +12,7 @@ EXPORTS chat_recv_msg_wait chat_parse_markdown chat_parse_server + chat_parse_uri chat_password_hash chat_valid_name chat_json_length diff --git a/simplex-chat.cabal b/simplex-chat.cabal index fb1dc5e5fc..cbd569f8b6 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -298,6 +298,7 @@ library , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* + , uri-bytestring >=0.3.3.1 && <0.4 , uuid ==1.3.* , zip ==2.0.* , zstd ==0.1.3.* diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index 67e38bab15..19e80e4339 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -1,6 +1,8 @@ {-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} @@ -12,11 +14,13 @@ module Simplex.Chat.Markdown where import Control.Applicative (optional, (<|>)) +import Control.Monad import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A +import qualified Data.ByteString.Char8 as B import Data.Char (isAlpha, isAscii, isDigit, isPunctuation, isSpace) import Data.Either (fromRight) import Data.Functor (($>)) @@ -34,9 +38,10 @@ import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnReqUriData (. import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, sumTypeJSON) import Simplex.Messaging.Protocol (ProtocolServer (..)) -import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8) +import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8, tshow) import System.Console.ANSI.Types import qualified Text.Email.Validate as Email +import qualified URI.ByteString as U data Markdown = Markdown (Maybe Format) Text | Markdown :|: Markdown deriving (Eq, Show) @@ -49,7 +54,9 @@ data Format | Secret | Colored {color :: FormatColor} | Uri - | SimplexLink {linkType :: SimplexLinkType, simplexUri :: AConnectionLink, smpHosts :: NonEmpty Text} + -- showText is Nothing for the usual Uri without text + | HyperLink {showText :: Maybe Text, linkUri :: Text} + | SimplexLink {showText :: Maybe Text, linkType :: SimplexLinkType, simplexUri :: AConnectionLink, smpHosts :: NonEmpty Text} | Command {commandStr :: Text} | Mention {memberName :: Text} | Email @@ -187,6 +194,7 @@ markdownP = mconcat <$> A.many' fragmentP '!' -> coloredP <|> wordP '@' -> mentionP <|> wordP '/' -> commandP <|> wordP + '[' -> sowLinkP <|> wordP _ | isDigit c -> phoneP <|> wordP | otherwise -> wordP @@ -224,6 +232,20 @@ markdownP = mconcat <$> A.many' fragmentP let origStr = if c == '\'' then '\'' `T.cons` str `T.snoc` '\'' else str res = markdown (format str) (pfx `T.cons` origStr) pure $ if T.null punct then res else res :|: unmarked punct + sowLinkP = do + t <- '[' `inParens` ']' + l <- '(' `inParens` ')' + let hasPunct = T.any (\c -> isPunctuation c && c /= '-' && c /= '_') t + when (hasPunct && t /= l && ("https://" <> t) /= l) $ fail "punctuation in hyperlink text" + f <- case strDecode $ encodeUtf8 l of + Right lnk@(ACL _ cLink) -> case cLink of + CLShort _ -> pure $ simplexUriFormat (Just t) lnk + CLFull _ -> fail "full SimpleX link in hyperlink" + Left _ -> case parseUri $ encodeUtf8 l of + Right _ -> pure $ HyperLink (Just t) l + Left e -> fail $ "not uri: " <> T.unpack e + pure $ markdown f $ T.concat ["[", t, "](", l, ")"] + inParens open close = A.char open *> A.takeWhile1 (/= close) <* A.char close colorP = A.anyChar >>= \case 'r' -> optional "ed" $> Red @@ -253,7 +275,11 @@ markdownP = mconcat <$> A.many' fragmentP wordMD :: Text -> Markdown wordMD s | T.null s = unmarked s - | isUri s' = res $ uriMarkdown s' + | isUri s' = case strDecode $ encodeUtf8 s of + Right cLink -> res $ markdown (simplexUriFormat Nothing cLink) s' + Left _ -> case parseUri $ encodeUtf8 s' of + Right _ -> res $ markdown Uri s' + Left _ -> unmarked s | isDomain s' = res $ markdown Uri s' | isEmail s' = res $ markdown Email s' | otherwise = unmarked s @@ -265,9 +291,6 @@ markdownP = mconcat <$> A.many' fragmentP '/' -> False ')' -> False c -> isPunctuation c - uriMarkdown s = case strDecode $ encodeUtf8 s of - Right cLink -> markdown (simplexUriFormat cLink) s - _ -> markdown Uri s isUri s = T.length s >= 10 && any (`T.isPrefixOf` s) ["http://", "https://", "simplex:/"] -- matches what is likely to be a domain, not all valid domain names isDomain s = case T.splitOn "." s of @@ -281,11 +304,11 @@ markdownP = mconcat <$> A.many' fragmentP && (let p c = isAscii c && isAlpha c in T.all p name && T.all p tld) isEmail s = T.any (== '@') s && Email.isValid (encodeUtf8 s) noFormat = pure . unmarked - simplexUriFormat :: AConnectionLink -> Format - simplexUriFormat = \case + simplexUriFormat :: Maybe Text -> AConnectionLink -> Format + simplexUriFormat showText = \case ACL m (CLFull cReq) -> case cReq of - CRContactUri crData -> SimplexLink (linkType' crData) cLink $ uriHosts crData - CRInvitationUri crData _ -> SimplexLink XLInvitation cLink $ uriHosts crData + CRContactUri crData -> SimplexLink showText (linkType' crData) cLink $ uriHosts crData + CRInvitationUri crData _ -> SimplexLink showText XLInvitation cLink $ uriHosts crData where cLink = ACL m $ CLFull $ simplexConnReqUri cReq uriHosts ConnReqUriData {crSmpQueues} = L.map strEncodeText $ sconcat $ L.map (host . qServer) crSmpQueues @@ -293,8 +316,8 @@ markdownP = mconcat <$> A.many' fragmentP Just (CRDataGroup _) -> XLGroup Nothing -> XLContact ACL m (CLShort sLnk) -> case sLnk of - CSLContact _ ct srv _ -> SimplexLink (linkType' ct) cLink $ uriHosts srv - CSLInvitation _ srv _ _ -> SimplexLink XLInvitation cLink $ uriHosts srv + CSLContact _ ct srv _ -> SimplexLink showText (linkType' ct) cLink $ uriHosts srv + CSLInvitation _ srv _ _ -> SimplexLink showText XLInvitation cLink $ uriHosts srv where cLink = ACL m $ CLShort $ simplexShortLink sLnk uriHosts srv = L.map strEncodeText $ host srv @@ -305,6 +328,24 @@ markdownP = mconcat <$> A.many' fragmentP strEncodeText :: StrEncoding a => a -> Text strEncodeText = safeDecodeUtf8 . strEncode +parseUri :: B.ByteString -> Either Text U.URI +parseUri s = case U.parseURI U.laxURIParserOptions s of + Left e -> Left $ "Invalid URI: " <> tshow e + Right uri@U.URI {uriScheme = U.Scheme sch, uriAuthority} + | sch /= "http" && sch /= "https" -> Left $ "Unsupported URI scheme: " <> safeDecodeUtf8 sch + | otherwise -> case uriAuthority of + Nothing -> Left "No URI host" + Just U.Authority {authorityHost = U.Host h} + | '.' `B.notElem` h -> Left $ "Invalid URI host: " <> safeDecodeUtf8 h + | otherwise -> Right uri + +sanitizeUri :: U.URI -> Maybe U.URI +sanitizeUri uri@U.URI {uriQuery = U.Query originalQS} = + let sanitizedQS = filter (\(p, _) -> p == "q" || p == "search") originalQS + in if length sanitizedQS == length originalQS + then Nothing + else Just $ uri {U.uriQuery = U.Query sanitizedQS} + markdownText :: FormattedText -> Text markdownText (FormattedText f_ t) = case f_ of Nothing -> t @@ -316,6 +357,7 @@ markdownText (FormattedText f_ t) = case f_ of Secret -> around '#' Colored (FormatColor c) -> color c Uri -> t + HyperLink {} -> t SimplexLink {} -> t Mention _ -> t Command _ -> t diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 5d9af59b23..23712cf992 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -1,5 +1,6 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} @@ -25,6 +26,7 @@ import Data.Functor (($>)) import Data.List (find) import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) +import Data.Text (Text) import Data.Word (Word8) import Foreign.C.String import Foreign.C.Types (CInt (..)) @@ -35,7 +37,7 @@ import GHC.IO.Encoding (setFileSystemEncoding, setForeignEncoding, setLocaleEnco import Simplex.Chat import Simplex.Chat.Controller import Simplex.Chat.Library.Commands -import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList) +import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList, parseUri, sanitizeUri) import Simplex.Chat.Mobile.File import Simplex.Chat.Mobile.Shared import Simplex.Chat.Mobile.WebRTC @@ -56,6 +58,7 @@ import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..) import Simplex.Messaging.Util (catchAll, liftEitherWith, safeDecodeUtf8) import System.IO (utf8) import System.Timeout (timeout) +import qualified URI.ByteString as U #if !defined(dbPostgres) import Data.ByteArray (ScrubbedBytes) import Database.SQLite.Simple (SQLError (..)) @@ -81,6 +84,20 @@ eitherToResult :: Maybe RemoteHostId -> Either ChatError r -> APIResult r eitherToResult rhId = either (APIError rhId) (APIResult rhId) {-# INLINE eitherToResult #-} +data ParsedUri = ParsedUri + { uriInfo :: Maybe UriInfo, + parseError :: Text + } + +data UriInfo = UriInfo + { scheme :: Text, + sanitized :: Maybe Text + } + +$(JQ.deriveJSON defaultJSON ''UriInfo) + +$(JQ.deriveJSON defaultJSON ''ParsedUri) + $(pure []) instance ToJSON r => ToJSON (APIResult r) where @@ -111,6 +128,8 @@ foreign export ccall "chat_parse_markdown" cChatParseMarkdown :: CString -> IO C foreign export ccall "chat_parse_server" cChatParseServer :: CString -> IO CJSONString +foreign export ccall "chat_parse_uri" cChatParseUri :: CString -> IO CJSONString + foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CString -> IO CString foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString @@ -200,6 +219,10 @@ cChatParseMarkdown s = newCStringFromLazyBS . chatParseMarkdown =<< B.packCStrin cChatParseServer :: CString -> IO CJSONString cChatParseServer s = newCStringFromLazyBS . chatParseServer =<< B.packCString s +-- | parse web URI - returns ParsedUri JSON +cChatParseUri :: CString -> IO CJSONString +cChatParseUri s = newCStringFromLazyBS . chatParseUri =<< B.packCString s + cChatPasswordHash :: CString -> CString -> IO CString cChatPasswordHash cPwd cSalt = do pwd <- B.packCString cPwd @@ -293,6 +316,7 @@ chatMigrateInitKey chatDbOpts keepKey confirm backgroundMode = runExceptT $ do DB.ErrorNotADatabase -> Left $ DBMErrorNotADatabase errDbStr _ -> dbError e #endif + dbError :: Show e => e -> Either DBMigrationResult DBStore dbError e = Left . DBMErrorSQL errDbStr $ show e chatCloseStore :: ChatController -> IO String @@ -342,6 +366,14 @@ chatParseServer = J.encode . toServerAddress . strDecode enc :: StrEncoding a => a -> String enc = B.unpack . strEncode +chatParseUri :: ByteString -> JSONByteString +chatParseUri s = J.encode $ case parseUri s of + Left e -> ParsedUri Nothing e + Right uri@U.URI {uriScheme = U.Scheme sch} -> + let sanitized = safeDecodeUtf8 . U.serializeURIRef' <$> sanitizeUri uri + uriInfo = UriInfo {scheme = safeDecodeUtf8 sch, sanitized} + in ParsedUri (Just uriInfo) "" + chatPasswordHash :: ByteString -> ByteString -> ByteString chatPasswordHash pwd salt = either (const "") passwordHash salt' where diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index 31dc3d668a..d6a14391b0 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -20,6 +20,7 @@ markdownTests = do secretText textColor textWithUri + textWithHyperlink textWithEmail textWithPhone textWithMentions @@ -172,11 +173,11 @@ uri :: Text -> Markdown uri = Markdown $ Just Uri simplexLink :: SimplexLinkType -> Text -> NonEmpty Text -> Text -> Markdown -simplexLink linkType uriText smpHosts t = Markdown (simplexLinkFormat linkType uriText smpHosts) t +simplexLink linkType uriText smpHosts t = Markdown (simplexLinkFormat linkType uriText smpHosts Nothing) t -simplexLinkFormat :: SimplexLinkType -> Text -> NonEmpty Text -> Maybe Format -simplexLinkFormat linkType uriText smpHosts = case strDecode $ encodeUtf8 uriText of - Right simplexUri -> Just SimplexLink {linkType, simplexUri, smpHosts} +simplexLinkFormat :: SimplexLinkType -> Text -> NonEmpty Text -> Maybe Text -> Maybe Format +simplexLinkFormat linkType uriText smpHosts showText = case strDecode $ encodeUtf8 uriText of + Right simplexUri -> Just SimplexLink {linkType, simplexUri, smpHosts, showText} Left e -> error e textWithUri :: Spec @@ -210,6 +211,7 @@ textWithUri = describe "text with Uri" do "www." <==> "www." ".com" <==> ".com" "example.academytoolong" <==> "example.academytoolong" + "simplex:/example" <==> "simplex:/example" it "SimpleX links" do let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" ("https://simplex.chat" <> inv) <==> simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://simplex.chat" <> inv) @@ -222,6 +224,27 @@ textWithUri = describe "text with Uri" do ("https://simplex.chat" <> gr) <==> simplexLink XLGroup ("simplex:" <> gr) ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("https://simplex.chat" <> gr) ("simplex:" <> gr) <==> simplexLink XLGroup ("simplex:" <> gr) ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("simplex:" <> gr) +web :: Text -> Text -> Text -> Markdown +web t u = Markdown $ Just HyperLink {showText = Just t, linkUri = u} + +textWithHyperlink :: Spec +textWithHyperlink = describe "text with HyperLink without link text" do + let addr = "https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw" + addr' = "simplex:/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw?h=smp6.simplex.im" + it "correct markdown" do + "[click here](https://example.com)" <==> web "click here" "https://example.com" "[click here](https://example.com)" + "For details [click here](https://example.com)" <==> "For details " <> web "click here" "https://example.com" "[click here](https://example.com)" + "[example.com](https://example.com)" <==> web "example.com" "https://example.com" "[example.com](https://example.com)" + "[example.com/page](https://example.com/page)" <==> web "example.com/page" "https://example.com/page" "[example.com/page](https://example.com/page)" + ("[Connect to me](" <> addr <> ")") <==> Markdown (simplexLinkFormat XLContact addr' ["smp6.simplex.im"] (Just "Connect to me")) ("[Connect to me](" <> addr <> ")") + it "potentially spoofed link" do + "[https://example.com](https://another.com)" <==> "[https://example.com](https://another.com)" + "[example.com/page](https://another.com/page)" <==> "[example.com/page](https://another.com/page)" + ("[Connect.to.me](" <> addr <> ")") <==> Markdown Nothing ("[Connect.to.me](" <> addr <> ")") + it "ignored as markdown" do + "[click here](example.com)" <==> "[click here](example.com)" + "[click here](https://example.com )" <==> "[click here](https://example.com )" + email :: Text -> Markdown email = Markdown $ Just Email @@ -330,7 +353,7 @@ multilineMarkdownList = describe "multiline markdown" do it "multiline with simplex link" do ("https://simplex.chat" <> inv <> "\ntext") <<==>> - [ FormattedText (simplexLinkFormat XLInvitation ("simplex:" <> inv) ["smp.simplex.im"]) ("https://simplex.chat" <> inv), + [ FormattedText (simplexLinkFormat XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] Nothing) ("https://simplex.chat" <> inv), "\ntext" ] it "command markdown" do diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 11a89bc62e..0ba784a1fc 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -75,6 +75,9 @@ mobileTests = do it "should convert invalid name to a valid name" testValidNameCApi describe "JSON length" $ do it "should compute length of JSON encoded string" testChatJsonLengthCApi + describe "Parsers" $ do + it "should parse server address" testChatParseServer + it "should parse and sanitize URI" testChatParseUri noActiveUser :: LB.ByteString noActiveUser = @@ -318,6 +321,14 @@ testChatJsonLengthCApi _ = do cInt2 <- cChatJsonLength =<< newCString "こんにちは!" cInt2 `shouldBe` 18 +testChatParseServer :: TestParams -> IO () +testChatParseServer _ = do + pure () + +testChatParseUri :: TestParams -> IO () +testChatParseUri _ = do + pure () + jDecode :: FromJSON a => String -> IO (Maybe a) jDecode = pure . J.decode . LB.pack From 76babe7791e8f68501d9ea8ff043746d8075895d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 9 Aug 2025 10:57:39 +0100 Subject: [PATCH 11/11] core: 6.4.3.0 (simplexmq 6.4.3.0) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index d12d4fbd25..c31fba995f 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 79ba60e3ad415f07568151f6b7756f721212502c + tag: 931c533a3ddb86345e95ac54e24df5474d9a349b source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index bb78f288c4..e47200a34f 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."79ba60e3ad415f07568151f6b7756f721212502c" = "1i8gw8fi9xslk7zgnl3xggwvqg5fdscpcfnjvf78d343nbq4c30k"; + "https://github.com/simplex-chat/simplexmq.git"."931c533a3ddb86345e95ac54e24df5474d9a349b" = "03s3gnb21fnlnmayy654aq56q4kwva48mfs3qacvr7asm8fpk2p3"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index cbd569f8b6..5d7e69cbf2 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.4.2.1 +version: 6.4.3.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat